From 0240832f5c3d9ddcdcd51c66edbf8cd889a54108 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Fri, 13 Sep 2019 13:28:50 -0400 Subject: [PATCH] internal/lsp/protocol: bring the typescript code up to date These versions of go.ts and requests.ts generate tsprotocol.go, tsserver.go, and tsclient.go. README.md now gives the version of the vscode source used, showing that the typescript code and tsprotocol.go are matched to the same git commit of vscode. Many of the diffs are just whitespace from vscode's formatting. Fixes golang/go#34225 Change-Id: Ib66dad9476b452f332a4c0e990faf2c6060a588e Reviewed-on: https://go-review.googlesource.com/c/tools/+/195297 Reviewed-by: Rebecca Stambler --- internal/lsp/protocol/typescript/README.md | 9 +- internal/lsp/protocol/typescript/go.ts | 72 +++++++++---- internal/lsp/protocol/typescript/requests.ts | 106 +++++++++++++------ 3 files changed, 132 insertions(+), 55 deletions(-) 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()