blob: 373b925619eec45229d2b8902b6c89a5e24420a1 [file] [log] [blame]
import * as fs from 'fs';
import * as ts from 'typescript';
// generate tsclient.go and tsserver.go, which are the definitions and stubs for
// supporting the LPS protocol. These files have 3 sections:
// 1. define the Client or Server type
// 2. fill out the clientHandler or serveHandler which is basically a large
// switch on the requests and notifications received by the client/server.
// 3. The methods corresponding to these. (basically parse the request,
// call something, and perhaps send a response.)
let dir = process.env['HOME'];
let fnames = [
`/vscode-languageserver-node/protocol/src/protocol.ts`,
`/vscode-languageserver-node/jsonrpc/src/main.ts`
];
let fda: number, fdy: number; // file descriptors
function createOutputFiles() {
fda = fs.openSync('/tmp/ts-a', 'w') // dump of AST
fdy = fs.openSync('/tmp/ts-c', 'w') // unused, for debugging
}
function pra(s: string) {
return (fs.writeSync(fda, s))
}
function prb(s: string) {
return (fs.writeSync(fdy, s + '\n'))
}
let program: ts.Program;
function generate(files: string[], options: ts.CompilerOptions): void {
program = ts.createProgram(files, options);
program.getTypeChecker();
dumpAST(); // for debugging
// visit every sourceFile in the program, collecting information
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
ts.forEachChild(sourceFile, genStuff)
}
}
// when 4 args, they are param, result, error, registration options, e.g.:
// RequestType<TextDocumentPositionParams, Definition | DefinitionLink[] |
// null,
// void, TextDocumentRegistrationOptions>('textDocument/implementation');
// 3 args is RequestType0('shutdown')<void, void, void>
// and RequestType0('workspace/workspaceFolders)<WorkspaceFolder[]|null, void,
// void>
// the two args are the notification data and the registration data
// except for textDocument/selectionRange and a NotificationType0('exit')
// selectionRange is the following, but for now do it by hand, special case.
// RequestType<TextDocumentPositionParams, SelectionRange[] | null, any, any>
// = new RequestType('textDocument/selectionRange')
// and foldingRange has the same problem.
setReceives(); // distinguish client and server
// for each of Client and Server there are 3 parts to the output:
// 1. type X interface {methods}
// 2. func (h *serverHandler) Deliver(...) { switch r.method }
// 3. func (x *xDispatcher) Method(ctx, parm)
not.forEach(
(v, k) => {receives.get(k) == 'client' ? goNot(client, k) :
goNot(server, k)});
req.forEach(
(v, k) => {receives.get(k) == 'client' ? goReq(client, k) :
goReq(server, k)});
// and print the Go code
output(client);
output(server);
return;
}
// Go signatures for methods.
function sig(nm: string, a: string, b: string, names?: boolean): string {
if (a != '') {
if (names)
a = ', params *' + a;
else
a = ', *' + a;
}
let ret = 'error';
if (b != '') {
b.startsWith('[]') || b.startsWith('interface') || (b = '*' + b);
ret = `(${b}, error)`;
}
let start = `${nm}(`;
if (names) {
start = start + 'ctx ';
}
return `${start}context.Context${a}) ${ret}`;
}
const notNil = `if r.Params != nil {
r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params"))
return true
}`;
// Go code for notifications. Side is client or server, m is the request method
function goNot(side: side, m: string) {
const n = not.get(m);
let a = goType(side, m, n.typeArguments[0]);
// let b = goType(m, n.typeArguments[1]); These are registration options
const nm = methodName(m);
side.methods.push(sig(nm, a, ''));
const caseHdr = `case "${m}": // notif`;
let case1 = notNil;
if (a != '') {
case1 = `var params ${a}
if err := json.Unmarshal(*r.Params, &params); err != nil {
sendParseError(ctx, r, err)
return true
}
if err := h.${side.name}.${nm}(ctx, &params); err != nil {
log.Error(ctx, "", err)
}
return true`;
} else {
case1 = `if err := h.${side.name}.${nm}(ctx); err != nil {
log.Error(ctx, "", err)
}
return true`;
}
side.cases.push(`${caseHdr}\n${case1}`);
const arg3 = a == '' ? 'nil' : 'params';
side.calls.push(`
func (s *${side.name}Dispatcher) ${sig(nm, a, '', true)} {
return s.Conn.Notify(ctx, "${m}", ${arg3})
}`);
}
// Go code for requests.
function goReq(side: side, m: string) {
const n = req.get(m);
const nm = methodName(m);
let a = goType(side, m, n.typeArguments[0]);
let b = goType(side, m, n.typeArguments[1]);
if (n.getText().includes('Type0')) {
b = a;
a = ''; // workspace/workspaceFolders and shutdown
}
prb(`${side.name} req ${a != ''},${b != ''} ${nm} ${m} ${loc(n)}`)
side.methods.push(sig(nm, a, b));
const caseHdr = `case "${m}": // req`;
let case1 = notNil;
if (a != '') {
case1 = `var params ${a}
if err := json.Unmarshal(*r.Params, &params); err != nil {
sendParseError(ctx, r, err)
return true
}`;
}
const arg2 = a == '' ? '' : ', &params';
let case2 = `if err := h.${side.name}.${nm}(ctx${arg2}); err != nil {
log.Error(ctx, "", err)
}`;
if (b != '') {
case2 = `resp, err := h.${side.name}.${nm}(ctx${arg2})
if err := r.Reply(ctx, resp, err); err != nil {
log.Error(ctx, "", err)
}
return true`;
} else { // response is nil
case2 = `err := h.${side.name}.${nm}(ctx${arg2})
if err := r.Reply(ctx, nil, err); err != nil {
log.Error(ctx, "", err)
}
return true`
}
side.cases.push(`${caseHdr}\n${case1}\n${case2}`);
const callHdr = `func (s *${side.name}Dispatcher) ${sig(nm, a, b, true)} {`;
let callBody = `return s.Conn.Call(ctx, "${m}", nil, nil)\n}`;
if (b != '') {
const p2 = a == '' ? 'nil' : 'params';
let theRet = `result`;
!b.startsWith('[]') && !b.startsWith('interface') && (theRet = '&result');
callBody = `var result ${b}
if err := s.Conn.Call(ctx, "${m}", ${
p2}, &result); err != nil {
return nil, err
}
return ${theRet}, nil
}`;
} else if (a != '') {
callBody = `return s.Conn.Call(ctx, "${m}", params, nil) // Call, not Notify
}`
}
side.calls.push(`${callHdr}\n${callBody}\n`);
}
// make sure method names are unique
let seenNames = new Set<string>();
function methodName(m: string): string {
const i = m.indexOf('/');
let s = m.substring(i + 1);
let x = s[0].toUpperCase() + s.substring(1);
if (seenNames.has(x)) {
x += m[0].toUpperCase() + m.substring(1, i);
}
seenNames.add(x);
return x;
}
function output(side: side) {
if (side.outputFile === undefined) side.outputFile = `ts${side.name}.go`;
side.fd = fs.openSync(side.outputFile, 'w');
const f = function(s: string) {
fs.writeSync(side.fd, s);
fs.writeSync(side.fd, '\n');
};
f(`package protocol`);
f(`// Code generated (see typescript/README.md) DO NOT EDIT.\n`);
f(`
import (
"context"
"encoding/json"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/xcontext"
)
`);
const a = side.name[0].toUpperCase() + side.name.substring(1)
f(`type ${a} interface {`);
side.methods.forEach((v) => {f(v)});
f('}\n');
f(`func (h ${
side.name}Handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
if delivered {
return false
}
if ctx.Err() != nil {
ctx := xcontext.Detach(ctx)
r.Reply(ctx, nil, jsonrpc2.NewErrorf(RequestCancelledError, ""))
return true
}
switch r.Method {`);
side.cases.forEach((v) => {f(v)});
f(`
default:
return false
}
}`);
f(`
type ${side.name}Dispatcher struct {
*jsonrpc2.Conn
}
`);
side.calls.forEach((v) => {f(v)});
if (side.name == 'server')
f(`
type CancelParams struct {
/**
* The request id to cancel.
*/
ID jsonrpc2.ID \`json:"id"\`
}`);
f(`// Types constructed to avoid structs as formal argument types`)
side.ourTypes.forEach((val, key) => f(`type ${val} ${key}`));
}
interface side {
methods: string[];
cases: string[];
calls: string[];
ourTypes: Map<string, string>;
name: string; // client or server
goName: string; // Client or Server
outputFile?: string;
fd?: number
}
let client: side = {
methods: [],
cases: [],
calls: [],
name: 'client',
goName: 'Client',
ourTypes: new Map<string, string>()
};
let server: side = {
methods: [],
cases: [],
calls: [],
name: 'server',
goName: 'Server',
ourTypes: new Map<string, string>()
};
let req = new Map<string, ts.NewExpression>(); // requests
let not = new Map<string, ts.NewExpression>(); // notifications
let receives = new Map<string, 'server'|'client'>(); // who receives it
function setReceives() {
// mark them all as server, then adjust the client ones.
// it would be nice to have some independent check
req.forEach((_, k) => {receives.set(k, 'server')});
not.forEach((_, k) => {receives.set(k, 'server')});
receives.set('window/logMessage', 'client');
receives.set('telemetry/event', 'client');
receives.set('client/registerCapability', 'client');
receives.set('client/unregisterCapability', 'client');
receives.set('window/showMessage', 'client');
receives.set('window/showMessageRequest', 'client');
receives.set('workspace/workspaceFolders', 'client');
receives.set('workspace/configuration', 'client');
receives.set('workspace/applyEdit', 'client');
receives.set('textDocument/publishDiagnostics', 'client');
// a small check
receives.forEach((_, k) => {
if (!req.get(k) && !not.get(k)) throw new Error(`missing ${k}}`);
if (req.get(k) && not.get(k)) throw new Error(`dup ${k}`);
})
}
function goType(side: side, m: string, n: ts.Node): string {
if (n === undefined) return '';
if (ts.isTypeReferenceNode(n)) return n.typeName.getText();
if (n.kind == ts.SyntaxKind.VoidKeyword) return '';
if (n.kind == ts.SyntaxKind.AnyKeyword) return 'interface{}';
if (ts.isArrayTypeNode(n)) return '[]' + goType(side, m, n.elementType);
// special cases, before we get confused
switch (m) {
case 'textDocument/completion':
return 'CompletionList';
case 'textDocument/documentSymbol':
return '[]DocumentSymbol';
case 'textDocument/prepareRename':
return 'Range';
case 'textDocument/codeAction':
return '[]CodeAction';
}
if (ts.isUnionTypeNode(n)) {
let x: string[] = [];
n.types.forEach(
(v) => {v.kind != ts.SyntaxKind.NullKeyword &&
x.push(goType(side, m, v))});
if (x.length == 1) return x[0];
prb(`===========${m} ${x}`)
// Because we don't fully resolve types, we don't know that
// Definition is Location | Location[]
if (x[0] == 'Definition') return '[]Location';
if (x[1] == '[]' + x[0] + 'Link') return x[1];
throw new Error(`${m}, ${x} unexpected types`)
}
if (ts.isIntersectionTypeNode(n)) {
// we expect only TypeReferences, and put out a struct with embedded types
// This is not good, as it uses a struct where a type name ought to be.
let x: string[] = [];
n.types.forEach((v) => {
// expect only TypeReferences
if (!ts.isTypeReferenceNode(v)) {
throw new Error(
`expected only TypeReferences in Intersection ${getText(n)}`)
}
x.push(goType(side, m, v));
x.push(';')
})
x.push('}')
let ans = 'struct {'.concat(...x);
// If ans does not have a type, create it
if (side.ourTypes.get(ans) == undefined) {
side.ourTypes.set(ans, 'Param' + getText(n).substring(0, 6))
}
// Return the type
return side.ourTypes.get(ans)
}
return '?';
}
// walk the AST finding Requests and Notifications
function genStuff(node: ts.Node) {
if (!ts.isNewExpression(node)) {
ts.forEachChild(node, genStuff)
return;
}
// process the right kind of new expression
const wh = node.expression.getText();
if (wh != 'RequestType' && wh != 'RequestType0' && wh != 'NotificationType' &&
wh != 'NotificationType0')
return;
if (node.arguments === undefined || node.arguments.length != 1 ||
!ts.isStringLiteral(node.arguments[0])) {
throw new Error(`missing n.arguments ${loc(node)}`)
}
// RequestType<useful>=new RequestTYpe('foo')
if (node.typeArguments === undefined) {
node.typeArguments = lookUp(node);
}
// new RequestType<useful>
let s = node.arguments[0].getText();
// Request or Notification
const v = wh[0] == 'R' ? req : not;
s = s.substring(1, s.length - 1); // remove quoting
if (s == '$/cancelRequest') return; // special case in output
v.set(s, node);
}
// find the text of a node
function getText(node: ts.Node): string {
let sf = node.getSourceFile();
let start = node.getStart(sf)
let end = node.getEnd()
return sf.text.substring(start, end)
}
function lookUp(n: ts.NewExpression): ts.NodeArray<ts.TypeNode> {
// parent should be VariableDeclaration. its children should be
// Identifier('type') ???
// TypeReference: [Identifier('RequestType1), ]
// NewExpression (us)
const p = n.parent;
if (!ts.isVariableDeclaration(p)) throw new Error(`not variable decl`);
const tr = p.type;
if (!ts.isTypeReferenceNode(tr)) throw new Error(`not TypeReference`);
return tr.typeArguments;
}
function dumpAST() {
// dump the ast, for debugging
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
// walk the tree to do stuff
ts.forEachChild(sourceFile, describe);
}
}
}
// some tokens have the wrong default name
function strKind(n: ts.Node): string {
const x = ts.SyntaxKind[n.kind];
switch (x) {
default:
return x;
case 'FirstAssignment':
return 'EqualsToken';
case 'FirstBinaryOperator':
return 'LessThanToken';
case 'FirstCompoundAssignment':
return 'PlusEqualsToken';
case 'FirstContextualKeyword':
return 'AbstractKeyword';
case 'FirstLiteralToken':
return 'NumericLiteral';
case 'FirstNode':
return 'QualifiedName';
case 'FirstTemplateToken':
return 'NoSubstitutionTemplateLiteral';
case 'LastTemplateToken':
return 'TemplateTail';
case 'FirstTypeNode':
return 'TypePredicate';
}
}
function describe(node: ts.Node) {
if (node === undefined) {
return
}
let indent = '';
function f(n: ts.Node) {
if (ts.isIdentifier(n)) {
pra(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`)
} else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) {
pra(`${indent} ${loc(n)} ${strKind(n)} \n`)
} else if (ts.isTypeLiteralNode(n)) {
let m = n.members
pra(`${indent} ${loc(n)} ${strKind(n)} ${m.length} \n`)
} else {
pra(`${indent} ${loc(n)} ${strKind(n)} \n`)
};
indent += ' '
ts.forEachChild(n, f)
indent = indent.slice(0, indent.length - 2)
}
f(node)
}
// string version of the location in the source file
function loc(node: ts.Node): string {
const sf = node.getSourceFile();
const start = node.getStart()
const x = sf.getLineAndCharacterOfPosition(start)
const full = node.getFullStart()
const y = sf.getLineAndCharacterOfPosition(full)
let fn = sf.fileName
const n = fn.search(/-node./)
fn = fn.substring(n + 6)
return `${fn} ${x.line + 1}: ${x.character + 1} (${y.line + 1}: ${
y.character + 1})`
}
// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters
function main() {
let args = process.argv.slice(2) // effective command line
if (args.length > 0) {
let j = 0;
if (args[j] == '-d') {
dir = args[j + 1]
j += 2
}
if (j != args.length) throw new Error(`incomprehensible args ${args}`)
}
let files: string[] = [];
for (let i = 0; i < fnames.length; i++) {
files.push(`${dir}${fnames[i]}`)
}
createOutputFiles()
generate(
files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS});
}
main()