1
0
mirror of https://github.com/golang/go synced 2024-11-18 20:14:43 -07:00

internal/lsp.protocol: identify the version of the LSP that code is generated for

Changes go.ts to check that the commit hash of the vscode is the one that it
is expecting. README.md now contains more explanation.

Change-Id: Ia5a947c6d5d026c2b7d9ab18877c320e8a7f45d2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/195438
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
pjw 2019-09-14 15:18:05 -04:00 committed by Peter Weinberger
parent 92af9d69ef
commit e45ffcd953
2 changed files with 310 additions and 282 deletions

View File

@ -1,4 +1,4 @@
# Generate Go types for the LSP protocol
# Generate Go types and signatures for the LSP protocol
## Setup
@ -8,7 +8,6 @@
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
@ -23,13 +22,19 @@ and for simple checking
It defaults to `$(HOME)`.
`-o out.go` says where the generated go code goes.
It defaults to `/tmp/tsprotocol.go`.
It defaults to `tsprotocol.go`.
To generate the client and server boilerplate (tsclient.go and tsserver.go)
```tsc requests.ts && node requests.js [-d dir] && gofmt -w tsclient.go tsserver.go```
-d dir is the same as above. The output files are written into the current directory.
## Note
## Notes
`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.
1. `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.
2. Because the Typescript and Go type systems are incompatible, `go.ts ` and `request.ts` are filled with heuristics and special cases. Therefore they are tied to a specific commit of `vscode-languageserver-node`. The hash code of the commit is included in the header of `tsprotocol.go` and stored in the variable `gitHash` in `go.ts`. It is checked (see `git()` in `go.ts`) on every execution of `go.ts`.
3. Generating the `ts*.go` files is only semi-automated. Please file an issue if the released version is too far behind.
4. For the impatient, first change `gitHash` by hand (`git()` shows how to find the hash).
1. Then try to run `go.ts` and `requests.ts`. This will likely fail because the heuristics don't cover some new case. For instance, some simple type like `string` might have changed to a union type `string | [number,number]`. (Look at the `UnionTypeNode` code near line 588 of `go.ts`.) Another example is that some formal parameter generated by `requests.ts` will have anonymous structure type, which is essentially unusable. (See the code related to `ourTypes`.)
1. Next step is to try to move the generated code to `internal/lsp/protocol` and try to build `gopls` and its tests. This will likely fail because types have changed. Generally the fixes are fairly easy. (The code for `ourTypes` was a case where changes had to be made to `requests.ts`.)
1. Since there are not adequate integration tests, the next step is to run `gopls`. A common failure will be a nil dereference, because some previously simple default is now in an optional structure.

View File

@ -53,7 +53,8 @@ let fnames = [
`${srcDir}/protocol/src/protocol.ts`, `${srcDir}/types/src/main.ts`,
`${srcDir}/jsonrpc/src/main.ts`
];
let outFname = '/tmp/tsprotocol.go';
let gitHash = 'fda16d6b63ba0fbdbd21d437ea810685528a0018';
let outFname = 'tsprotocol.go';
let fda: number, fdb: number, fde: number; // file descriptors
function createOutputFiles() {
@ -290,7 +291,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
}
})
let goName = toGoName(id.text)
let {goType, gostuff, optional, fields} = computeType(thing)
let { goType, gostuff, optional, fields } = computeType(thing)
// Generics
if (gen && gen.text == goType) goType = 'interface{}';
opt = opt || optional;
@ -451,7 +452,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
} else
throw new Error(`expected TypeRef ${strKind(n)} ${loc(n)}`)
})
let ans = {me: node, name: toGoName(getText(id)), embeds: embeds};
let ans = { me: node, name: toGoName(getText(id)), embeds: embeds };
Structs.push(ans)
return
}
@ -522,25 +523,24 @@ function generate(files: string[], options: ts.CompilerOptions): void {
}
// complex and filled with heuristics
function computeType(node: ts.Node):
{goType: string, gostuff?: string, optional?: boolean, fields?: Field[]} {
function computeType(node: ts.Node): { goType: string, gostuff?: string, optional?: boolean, fields?: Field[] } {
switch (node.kind) {
case ts.SyntaxKind.AnyKeyword:
case ts.SyntaxKind.ObjectKeyword:
return {goType: 'interface{}'};
return { goType: 'interface{}' };
case ts.SyntaxKind.BooleanKeyword:
return {goType: 'bool'};
return { goType: 'bool' };
case ts.SyntaxKind.NumberKeyword:
return {goType: 'float64'};
return { goType: 'float64' };
case ts.SyntaxKind.StringKeyword:
return {goType: 'string'};
return { goType: 'string' };
case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.UndefinedKeyword:
return {goType: 'nil'};
return { goType: 'nil' };
}
if (ts.isArrayTypeNode(node)) {
let {goType, gostuff, optional} = computeType(node.elementType)
return ({goType: '[]' + goType, gostuff: gostuff, optional: optional})
let { goType, gostuff, optional } = computeType(node.elementType)
return ({ goType: '[]' + goType, gostuff: gostuff, optional: optional })
} else if (ts.isTypeReferenceNode(node)) {
// typeArguments?: NodeArray<TypeNode>;typeName: EntityName;
// typeArguments won't show up in the generated Go
@ -549,7 +549,7 @@ 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: toGoName(tn.text)};
return { goType: toGoName(tn.text) };
} else {
throw new Error(
`expected identifier got ${strKind(node.typeName)} at ${loc(tn)}`)
@ -562,7 +562,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (txt.charAt(0) == '\'') {
typ = 'string'
}
return {goType: typ, gostuff: getText(node)};
return { goType: typ, gostuff: getText(node) };
} else if (ts.isTypeLiteralNode(node)) {
// {[uri:string]: TextEdit[];} -> map[string][]TextEdit
let x: Field[] = [];
@ -582,13 +582,13 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (indexCnt != 1 || x.length != 1)
throw new Error(`undexpected Index ${loc(x[0].me)}`)
// instead of {map...} just the map
return ({goType: x[0].goType, gostuff: x[0].gostuff})
return ({ goType: x[0].goType, gostuff: x[0].gostuff })
}
return ({goType: 'embedded!', fields: x})
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))})
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
@ -596,18 +596,18 @@ function generate(files: string[], options: ts.CompilerOptions): void {
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: 'interface{}', gostuff: getText(node) })
}
return ({goType: 'bool', gostuff: getText(node)})
return ({ goType: 'bool', gostuff: getText(node) })
}
// these are special cases from looking at the source
let gostuff = getText(node);
if (x[0].goType == `"off"` || x[0].goType == 'string') {
return ({goType: 'string', gostuff: gostuff})
return ({ goType: 'string', gostuff: gostuff })
}
if (x[0].goType == 'TextDocumentSyncOptions') {
// TextDocumentSyncOptions | TextDocumentSyncKind
return ({goType: 'interface{}', gostuff: gostuff})
return ({ goType: 'interface{}', gostuff: gostuff })
}
if (x[0].goType == 'float64' && x[1].goType == 'string') {
return {
@ -686,7 +686,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (s.charAt(0) == '_') {
ans = 'Inner' + s.substring(1)
}
else {ans = s.substring(0, 1).toUpperCase() + s.substring(1)};
else { ans = s.substring(0, 1).toUpperCase() + s.substring(1) };
ans = ans.replace(/Uri$/, 'URI')
ans = ans.replace(/Id$/, 'ID')
return ans
@ -702,7 +702,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
// return a string of the kinds of the immediate descendants
function kinds(n: ts.Node): string {
let res = 'Seen ' + strKind(n);
function f(n: ts.Node): void{res += ' ' + strKind(n)};
function f(n: ts.Node): void { res += ' ' + strKind(n) };
ts.forEachChild(n, f)
return res
}
@ -752,7 +752,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
let m = n.members
pra(`${indent} ${loc(n)} ${strKind(n)} ${m.length}\n`)
}
else {pra(`${indent} ${loc(n)} ${strKind(n)}\n`)};
else { pra(`${indent} ${loc(n)} ${strKind(n)}\n`) };
indent += ' '
ts.forEachChild(n, f)
indent = indent.slice(0, indent.length - 2)
@ -769,14 +769,15 @@ function getComments(node: ts.Node): string {
return x
}
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)
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})`
}
@ -796,7 +797,7 @@ function emitTypes() {
}
let byName = new Map<string, Struct>();
function emitStructs() {
function emitStructs() {
dontEmit.set('Thenable', true);
dontEmit.set('EmitterOptions', true);
dontEmit.set('MessageReader', true);
@ -883,19 +884,19 @@ let byName = new Map<string, Struct>();
}
prgo(`}\n`);
}
}
}
function genComments(name: string, maybe: string): string {
function genComments(name: string, maybe: string): string {
if (maybe == '') return `\n\t// ${name} is\n`;
if (maybe.indexOf('/**') == 0) {
return maybe.replace('/**', `\n/*${name} defined:`)
}
throw new Error(`weird comment ${maybe.indexOf('/**')}`)
}
}
// Turn a Field into an output string
// flds is for merging
function strField(f: Field, noopt?: boolean, flds?: Field[]): string {
// 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) ? '*' : ''
switch (f.goType.charAt(0)) {
@ -951,9 +952,9 @@ let byName = new Map<string, Struct>();
ans.push(`\t} ${f.json}${stuff}\n`)
}
return (''.concat(...ans))
}
}
function emitConsts() {
function emitConsts() {
// 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.
@ -991,9 +992,9 @@ let byName = new Map<string, Struct>();
prgo(`\t${x} ${c.typeName} = ${c.value}\n`)
}
prgo(')\n')
}
}
function emitHeader(files: string[]) {
function emitHeader(files: string[]) {
let lastMod = 0
let lastDate: Date
for (const f of files) {
@ -1006,14 +1007,36 @@ let byName = new Map<string, Struct>();
let a = fs.readFileSync(`${dir}${srcDir}/.git/refs/heads/master`);
prgo(`// Package protocol contains data types and code for LSP jsonrpcs\n`)
prgo(`// generated automatically from vscode-languageserver-node\n`)
prgo(`// commit: ${a.toString()}`)
prgo(`// commit: ${gitHash}\n`)
prgo(`// last fetched ${lastDate}\n`)
prgo('package protocol\n\n')
prgo(`// Code generated (see typescript/README.md) DO NOT EDIT.\n`);
};
};
// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters
function main() {
function git(): string {
let a = fs.readFileSync(`${dir}${srcDir}/.git/HEAD`).toString();
// ref: refs/heads/foo, or a hash like cc12d1a1c7df935012cdef5d085cdba04a7c8ebe
if (a.charAt(a.length - 1) == '\n') {
a = a.substring(0, a.length - 1);
}
if (a.length == 40) {
return a // a hash
}
if (a.substring(0, 5) == 'ref: ') {
const fname = `${dir}${srcDir}/.git/` + a.substring(5);
let b = fs.readFileSync(fname).toString()
if (b.length == 41) {
return b.substring(0, 40);
}
}
throw new Error("failed to find the git commit hash")
}
// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters
function main() {
if (gitHash != git()) {
throw new Error(`git hash mismatch, wanted\n${gitHash} but source is at\n${git()}`)
}
let args = process.argv.slice(2) // effective command line
if (args.length > 0) {
let j = 0;
@ -1033,11 +1056,11 @@ let byName = new Map<string, Struct>();
}
createOutputFiles()
generate(
files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS});
files, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS });
emitHeader(files)
emitStructs()
emitConsts()
emitTypes()
}
}
main()
main()