diff --git a/internal/lsp/protocol/typescript/README.md b/internal/lsp/protocol/typescript/README.md index 486e09dbd6..e62ca38a8c 100644 --- a/internal/lsp/protocol/typescript/README.md +++ b/internal/lsp/protocol/typescript/README.md @@ -3,15 +3,16 @@ ## Setup 1. Make sure `node` is installed. - As explained at the [node site]( Node) + As explained at the [node site]() you may need `npm install @types/node` for the node runtime types -2. Install the typescript compiler, with `npm install typescript`. +2. Install the typescript compiler, with `npm install typescript` 3. Make sure `tsc` and `node` are in your execution path. 4. Get the typescript code for the jsonrpc protocol with `git clone git@github.com:microsoft/vscode-languageserver-node.git` +5. go.ts and requests.ts, and the files they generate, are from commit 8801c20b667945f455d7e023c71d2f741caeda25 ## Usage -To generated the protocol types (x/tools/internal/lsp/protocol/tsprotocol.go) +To generate the protocol types (x/tools/internal/lsp/protocol/tsprotocol.go) ```tsc go.ts && node go.js [-d dir] [-o out.go]``` and for simple checking @@ -31,4 +32,4 @@ To generate the client and server boilerplate (tsclient.go and tsserver.go) ## Note -`go.ts` uses the Typescript compiler's API, which is [introduced]( API) in their wiki. +`go.ts` and `requests.ts` use the Typescript compiler's API, which is [introduced](https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview) in their wiki. diff --git a/internal/lsp/protocol/typescript/go.ts b/internal/lsp/protocol/typescript/go.ts index 558cc7ca69..0f86cb9096 100644 --- a/internal/lsp/protocol/typescript/go.ts +++ b/internal/lsp/protocol/typescript/go.ts @@ -1,9 +1,8 @@ import * as fs from 'fs'; import * as ts from 'typescript'; -// 1. Everything that returns a Go thing should have unusable?: boolean -// 2. Remember what gets exported, and don't print the others (so _ can stay) -// 3. Merge all intersection types, and probably Heritage types too +// Need a general strategy for union types. This code tries (heuristically) +// to choose one, but sometimes defaults (heuristically) to interface{} interface Const { typeName: string // repeated in each const goType: string @@ -103,7 +102,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { // Ignore top-level items that produce no output if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) || ts.isImportDeclaration(node) || ts.isVariableStatement(node) || - ts.isExportDeclaration(node) || + ts.isExportDeclaration(node) || ts.isEmptyStatement(node) || node.kind == ts.SyntaxKind.EndOfFileToken) { return; } @@ -171,6 +170,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { Structs.push(ans) } + // called only from doClassDeclaration function fromPropDecl(node: ts.PropertyDeclaration): Field { let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name); let opt = node.questionToken != undefined; @@ -270,6 +270,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { return ans } + // optional gen is the contents of function genProp(node: ts.PropertySignature, gen: ts.Identifier): Field { let id: ts.Identifier let thing: ts.Node @@ -417,7 +418,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { function doTypeAlias(node: ts.TypeAliasDeclaration) { // these are all Export Identifier alias let id: ts.Identifier = node.name; - let alias: ts.Node = node.type; + let alias: ts.TypeNode = node.type; let ans = { me: node, id: id, @@ -427,11 +428,14 @@ function generate(files: string[], options: ts.CompilerOptions): void { }; if (ts.isUnionTypeNode(alias)) { ans.goType = weirdUnionType(alias) - if (id.text == 'DocumentFilter') if (ans.goType == undefined) { // these are mostly redundant; maybe sort them out later return } + if (ans.goType == 'interface{}') { + // we didn't know what to do, so explain the choice + ans.stuff = `// ` + getText(alias) + } Types.push(ans) return } @@ -441,8 +445,9 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isTypeReferenceNode(n)) { const s = toGoName(computeType(n).goType) embeds.push(s) - // It's here just for embedding, and not used independently - dontEmit.set(s, true); + // It's here just for embedding, and not used independently, maybe + // PJW! + // dontEmit.set(s, true); // PJW: do we need this? } else throw new Error(`expected TypeRef ${strKind(n)} ${loc(n)}`) }) @@ -492,13 +497,16 @@ function generate(files: string[], options: ts.CompilerOptions): void { aString = true; return; } + if (n.kind == ts.SyntaxKind.NumberKeyword || + n.kind == ts.SyntaxKind.StringKeyword) { + n.kind == ts.SyntaxKind.NumberKeyword ? aNumber = true : aString = true; + return + } bad = true }) if (bad) return; // none of these are useful (so far) if (aNumber) { - if (aString) - throw new Error( - `weirdUnionType is both number and string ${loc(node)}`); + if (aString) return 'interface{}'; return 'float64'; } if (aString) return 'string'; @@ -513,6 +521,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { return ans } + // complex and filled with heuristics function computeType(node: ts.Node): {goType: string, gostuff?: string, optional?: boolean, fields?: Field[]} { switch (node.kind) { @@ -540,13 +549,14 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isQualifiedName(tn)) { throw new Error(`qualified name at ${loc(node)}`); } else if (ts.isIdentifier(tn)) { - return {goType: tn.text}; + return {goType: toGoName(tn.text)}; } else { throw new Error( `expected identifier got ${strKind(node.typeName)} at ${loc(tn)}`) } } else if (ts.isLiteralTypeNode(node)) { // string|float64 (are there other possibilities?) + // as of 20190908: only see string const txt = getText(node); let typ = 'float64' if (txt.charAt(0) == '\'') { @@ -554,6 +564,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { } return {goType: typ, gostuff: getText(node)}; } else if (ts.isTypeLiteralNode(node)) { + // {[uri:string]: TextEdit[];} -> map[string][]TextEdit let x: Field[] = []; let indexCnt = 0 node.forEachChild((n: ts.Node) => { @@ -575,13 +586,16 @@ function generate(files: string[], options: ts.CompilerOptions): void { } return ({goType: 'embedded!', fields: x}) } else if (ts.isUnionTypeNode(node)) { + // The major heuristics let x = new Array<{goType: string, gostuff?: string, optiona?: boolean}>() node.forEachChild((n: ts.Node) => {x.push(computeType(n))}) if (x.length == 2 && x[1].goType == 'nil') { + // Foo | null, or Foo | undefined return x[0] // make it optional somehow? TODO } - if (x[0].goType == 'bool') { // take it - if (x[1].goType == 'RenameOptions') { + if (x[0].goType == 'bool') { // take it, mostly + if (x[1].goType == 'RenameOptions' || + x[1].goType == 'CodeActionOptions') { return ({goType: 'interface{}', gostuff: getText(node)}) } return ({goType: 'bool', gostuff: getText(node)}) @@ -592,6 +606,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { return ({goType: 'string', gostuff: gostuff}) } if (x[0].goType == 'TextDocumentSyncOptions') { + // TextDocumentSyncOptions | TextDocumentSyncKind return ({goType: 'interface{}', gostuff: gostuff}) } if (x[0].goType == 'float64' && x[1].goType == 'string') { @@ -617,12 +632,13 @@ function generate(files: string[], options: ts.CompilerOptions): void { throw new Error('in UnionType, weird') } else if (ts.isParenthesizedTypeNode(node)) { // check that this is (TextDocumentEdit | CreateFile | RenameFile | - // DeleteFile) TODO(pjw) + // DeleteFile) TODO(pjw) IT IS NOT! FIX THIS! ALSO: + // (variousOptions & StaticFegistrationOptions) return { goType: 'TextDocumentEdit', gostuff: getText(node) } } else if (ts.isTupleTypeNode(node)) { - // string | [number, number]. TODO(pjw): check it really is + // in string | [number, number]. TODO(pjw): check it really is return { goType: 'string', gostuff: getText(node) } @@ -774,7 +790,7 @@ function emitTypes() { let stuff = (t.stuff == undefined) ? '' : t.stuff; prgo(`// ${t.goName} is a type\n`) prgo(`${getComments(t.me)}`) - prgo(`type ${t.goName} ${t.goType}${stuff}\n`) + prgo(`type ${t.goName} = ${t.goType}${stuff}\n`) seenConstTypes.set(t.goName, true); } } @@ -856,7 +872,7 @@ let byName = new Map(); fields.set(f.goName, x); } } - fields.forEach((val) => { + fields.forEach((val, key) => { if (val.length > 1) { // merge the fields with the same name prgo(strField(val[0], noopt, val)); @@ -878,6 +894,7 @@ let byName = new Map(); } // Turn a Field into an output string + // flds is for merging function strField(f: Field, noopt?: boolean, flds?: Field[]): string { let ans: string[] = []; let opt = (!noopt && f.optional) ? '*' : '' @@ -895,6 +912,20 @@ let byName = new Map(); ans.push(`\t${f.goName} ${opt}${f.goType} ${f.json}${stuff}\n`) } else if (flds !== undefined) { + // The logic that got us here is imprecise, so it is possible that + // the fields are really all the same, and don't need to be + // combined into a struct. + let simple = true; + for (const ff of flds) { + if (ff.substruct !== undefined || byName.get(ff.goType) !== undefined) { + simple = false + break + } + } + if (simple) { + // should check that the ffs are really all the same + return strField(flds[0], noopt) + } ans.push(`\t${f.goName} ${opt}struct{\n`); for (const ff of flds) { if (ff.substruct !== undefined) { @@ -926,8 +957,9 @@ let byName = new Map(); // need the consts too! Generate modifying prefixes and suffixes to ensure // consts are unique. (Go consts are package-level, but Typescript's are // not.) Use suffixes to minimize changes to gopls. - let pref = new Map( - [['DiagnosticSeverity', 'Severity'], ['WatchKind', 'Watch']]) // typeName->prefix + let pref = new Map([ + ['DiagnosticSeverity', 'Severity'], ['WatchKind', 'Watch'] + ]) // typeName->prefix let suff = new Map([ ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'] ]) diff --git a/internal/lsp/protocol/typescript/requests.ts b/internal/lsp/protocol/typescript/requests.ts index 8580629ca6..0115568f70 100644 --- a/internal/lsp/protocol/typescript/requests.ts +++ b/internal/lsp/protocol/typescript/requests.ts @@ -29,6 +29,7 @@ function prb(s: string) { } let program: ts.Program; + function generate(files: string[], options: ts.CompilerOptions): void { program = ts.createProgram(files, options); program.getTypeChecker(); @@ -62,15 +63,11 @@ function generate(files: string[], options: ts.CompilerOptions): void { // 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) - }); + (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) - }); + (v, k) => {receives.get(k) == 'client' ? goReq(client, k) : + goReq(server, k)}); // and print the Go code output(client); output(server); @@ -104,7 +101,7 @@ const notNil = `if r.Params != nil { // 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(m, n.typeArguments[0]); + 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, '')); @@ -140,8 +137,8 @@ function goReq(side: side, m: string) { const n = req.get(m); const nm = methodName(m); - let a = goType(m, n.typeArguments[0]); - let b = goType(m, n.typeArguments[1]); + 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 @@ -186,7 +183,7 @@ function goReq(side: side, m: string) { !b.startsWith('[]') && !b.startsWith('interface') && (theRet = '&result'); callBody = `var result ${b} if err := s.Conn.Call(ctx, "${m}", ${ - p2}, &result); err != nil { + p2}, &result); err != nil { return nil, err } return ${theRet}, nil @@ -214,7 +211,7 @@ function methodName(m: string): string { 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) { + const f = function(s: string) { fs.writeSync(side.fd, s); fs.writeSync(side.fd, '\n'); }; @@ -231,9 +228,10 @@ function output(side: side) { `); const a = side.name[0].toUpperCase() + side.name.substring(1) f(`type ${a} interface {`); - side.methods.forEach((v) => { f(v) }); + side.methods.forEach((v) => {f(v)}); f('}\n'); - f(`func (h ${side.name}Handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool { + f(`func (h ${ + side.name}Handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool { if delivered { return false } @@ -246,7 +244,7 @@ function output(side: side) { } r.Conn().Cancel(params.ID) return true`); - side.cases.forEach((v) => { f(v) }); + side.cases.forEach((v) => {f(v)}); f(` default: return false @@ -257,7 +255,7 @@ function output(side: side) { *jsonrpc2.Conn } `); - side.calls.forEach((v) => { f(v) }); + side.calls.forEach((v) => {f(v)}); if (side.name == 'server') f(` type CancelParams struct { @@ -266,31 +264,46 @@ function output(side: side) { */ 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; name: string; // client or server goName: string; // Client or Server outputFile?: string; fd?: number } -let client: side = - { methods: [], cases: [], calls: [], name: 'client', goName: 'Client' }; -let server: side = - { methods: [], cases: [], calls: [], name: 'server', goName: 'Server' }; +let client: side = { + methods: [], + cases: [], + calls: [], + name: 'client', + goName: 'Client', + ourTypes: new Map() +}; +let server: side = { + methods: [], + cases: [], + calls: [], + name: 'server', + goName: 'Server', + ourTypes: new Map() +}; let req = new Map(); // requests let not = new Map(); // notifications -let receives = new Map(); // who receives it +let receives = new Map(); // 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') }); + 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'); @@ -308,12 +321,12 @@ function setReceives() { }) } -function goType(m: string, n: ts.Node): string { +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(m, n.elementType); + if (ts.isArrayTypeNode(n)) return '[]' + goType(side, m, n.elementType); // special cases, before we get confused switch (m) { case 'textDocument/completion': @@ -328,7 +341,8 @@ function goType(m: string, n: ts.Node): string { if (ts.isUnionTypeNode(n)) { let x: string[] = []; n.types.forEach( - (v) => { v.kind != ts.SyntaxKind.NullKeyword && x.push(goType(m, v)) }); + (v) => {v.kind != ts.SyntaxKind.NullKeyword && + x.push(goType(side, m, v))}); if (x.length == 1) return x[0]; prb(`===========${m} ${x}`) @@ -338,6 +352,28 @@ function goType(m: string, n: ts.Node): string { 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 '?'; } @@ -350,10 +386,10 @@ function genStuff(node: ts.Node) { // process the right kind of new expression const wh = node.expression.getText(); if (wh != 'RequestType' && wh != 'RequestType0' && wh != 'NotificationType' && - wh != 'NotificationType0') + wh != 'NotificationType0') return; if (node.arguments === undefined || node.arguments.length != 1 || - !ts.isStringLiteral(node.arguments[0])) { + !ts.isStringLiteral(node.arguments[0])) { throw new Error(`missing n.arguments ${loc(node)}`) } // RequestType=new RequestTYpe('foo') @@ -369,6 +405,14 @@ function genStuff(node: ts.Node) { 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 { // parent should be VariableDeclaration. its children should be // Identifier('type') ??? @@ -453,7 +497,7 @@ function loc(node: ts.Node): string { const n = fn.search(/-node./) fn = fn.substring(n + 6) return `${fn} ${x.line + 1}: ${x.character + 1} (${y.line + 1}: ${ - y.character + 1})` + y.character + 1})` } // ad hoc argument parsing: [-d dir] [-o outputfile], and order matters @@ -473,7 +517,7 @@ function main() { } createOutputFiles() generate( - files, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }); + files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS}); } main()