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:
parent
92af9d69ef
commit
e45ffcd953
@ -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.
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user