internal/lsp/protocol: bring the code generating programs up to date

This is the typescript code that generates the current versions of
tsprotocol.go, tsserver.go, and tsclient.go.

Change-Id: If40cd7a46e5e7d646d99670da5e04831b6ddc222
Reviewed-on: https://go-review.googlesource.com/c/tools/+/180477
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Peter Weinberger 2019-06-03 14:59:40 -04:00
parent d4e310b4a8
commit 027b3b4d7b
3 changed files with 890 additions and 292 deletions

View File

@ -3,14 +3,15 @@
## Setup ## Setup
1. Make sure `node` is installed. 1. Make sure `node` is installed.
* As explained at the [node site](https://nodejs.org Node) As explained at the [node site](<https://nodejs.org> Node)
* You may need `node install @types/node` for the node runtime types you may need `node install @types/node` for the node runtime types
2. Install the typescript compiler, with `node install typescript`. 2. Install the typescript compiler, with `node install typescript`.
3. Make sure `tsc` and `node` are in your execution path. 3. Make sure `tsc` and `node` are in your execution path.
4. Get the typescript code for the jsonrpc protocol with `git clone vscode-lanuageserver-node.git` 4. Get the typescript code for the jsonrpc protocol with `git clone vscode-lanuageserver-node.git`
## Usage ## Usage
To generated the protocol types (x/tools/internal/lsp/protocol/tsprotocol.go)
```tsc go.ts && node go.js [-d dir] [-o out.go]``` ```tsc go.ts && node go.js [-d dir] [-o out.go]```
and for simple checking and for simple checking
@ -23,8 +24,11 @@ It defaults to `$(HOME)`.
`-o out.go` says where the generated go code goes. `-o out.go` says where the generated go code goes.
It defaults to `/tmp/tsprotocol.go`. It defaults to `/tmp/tsprotocol.go`.
(The output file cannot yet be used to build `gopls`. That will be fixed in a future CL.) 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 ## Note
`go.ts` uses the Typescript compiler's API, which is [introduced](https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview API) in their wiki. `go.ts` uses the Typescript compiler's API, which is [introduced](<https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview> API) in their wiki.

View File

@ -1,7 +1,9 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as ts from 'typescript'; 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
interface Const { interface Const {
typeName: string // repeated in each const typeName: string // repeated in each const
goType: string goType: string
@ -15,8 +17,9 @@ let seenConstTypes = new Map<string, boolean>();
interface Struct { interface Struct {
me: ts.Node me: ts.Node
name: string name: string
embeds: string[] embeds?: string[]
fields?: Field[] fields?: Field[];
extends?: string[]
} }
let Structs: Struct[] = []; let Structs: Struct[] = [];
@ -46,9 +49,10 @@ function seenAdd(x: string) {
} }
let dir = process.env['HOME']; let dir = process.env['HOME'];
const srcDir = '/vscode-languageserver-node'
let fnames = [ let fnames = [
`/vscode-languageserver-node/protocol/src/protocol.ts`, `${srcDir}/protocol/src/protocol.ts`, `${srcDir}/types/src/main.ts`,
`/vscode-languageserver-node/types/src/main.ts` `${srcDir}/jsonrpc/src/main.ts`
]; ];
let outFname = '/tmp/tsprotocol.go'; let outFname = '/tmp/tsprotocol.go';
let fda: number, fdb: number, fde: number; // file descriptors let fda: number, fdb: number, fde: number; // file descriptors
@ -68,6 +72,9 @@ function prgo(s: string) {
return (fs.writeSync(fde, s)) return (fs.writeSync(fde, s))
} }
// struct names that don't need to go in the output
let dontEmit = new Map<string, boolean>();
function generate(files: string[], options: ts.CompilerOptions): void { function generate(files: string[], options: ts.CompilerOptions): void {
let program = ts.createProgram(files, options); let program = ts.createProgram(files, options);
program.getTypeChecker(); // used for side-effects program.getTypeChecker(); // used for side-effects
@ -93,7 +100,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
return; return;
function genTypes(node: ts.Node) { function genTypes(node: ts.Node) {
// Ignore top-level items with no output // Ignore top-level items that produce no output
if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) || if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) ||
ts.isImportDeclaration(node) || ts.isVariableStatement(node) || ts.isImportDeclaration(node) || ts.isVariableStatement(node) ||
ts.isExportDeclaration(node) || ts.isExportDeclaration(node) ||
@ -112,18 +119,17 @@ function generate(files: string[], options: ts.CompilerOptions): void {
} else if (ts.isClassDeclaration(node)) { } else if (ts.isClassDeclaration(node)) {
doClassDeclaration(node) doClassDeclaration(node)
} else { } else {
throw new Error(`unexpected ${ts.SyntaxKind[node.kind]} ${loc(node)}`) throw new Error(`unexpected ${strKind(node)} ${loc(node)}`)
} }
} }
function doClassDeclaration(node: ts.ClassDeclaration) { function doClassDeclaration(node: ts.ClassDeclaration) {
let id: ts.Identifier let id: ts.Identifier = node.name;
let props = new Array<ts.PropertyDeclaration>() let props = new Array<ts.PropertyDeclaration>()
let extend: ts.HeritageClause; let extend: ts.HeritageClause;
let bad = false let bad = false
node.forEachChild((n: ts.Node) => { node.forEachChild((n: ts.Node) => {
if (ts.isIdentifier(n)) { if (ts.isIdentifier(n)) {
id = n;
return return
} }
if (ts.isPropertyDeclaration(n)) { if (ts.isPropertyDeclaration(n)) {
@ -134,20 +140,24 @@ function generate(files: string[], options: ts.CompilerOptions): void {
return return
} }
if (n.kind == ts.SyntaxKind.Constructor || ts.isMethodDeclaration(n) || if (n.kind == ts.SyntaxKind.Constructor || ts.isMethodDeclaration(n) ||
ts.isGetAccessor(n) || ts.isTypeParameterDeclaration(n)) { ts.isGetAccessor(n) || ts.isSetAccessor(n) ||
ts.isTypeParameterDeclaration(n)) {
bad = true; bad = true;
return return
} }
if (ts.isHeritageClause(n)) { if (ts.isHeritageClause(n)) {
extend = n;
return return
} }
if (n.kind == ts.SyntaxKind.AbstractKeyword) {
bad = true; // we think all of these are useless, but are unsure
return;
}
throw new Error(`doClass ${loc(n)} ${kinds(n)}`) throw new Error(`doClass ${loc(n)} ${kinds(n)}`)
}) })
if (bad) { if (bad) {
// the class is not useful for Go. // the class is not useful for Go.
return return
} // might we want the PropertyDecls? (don't think so) }
let fields: Field[] = []; let fields: Field[] = [];
for (const pr of props) { for (const pr of props) {
fields.push(fromPropDecl(pr)) fields.push(fromPropDecl(pr))
@ -155,37 +165,26 @@ function generate(files: string[], options: ts.CompilerOptions): void {
let ans = { let ans = {
me: node, me: node,
name: toGoName(getText(id)), name: toGoName(getText(id)),
embeds: heritageStrs(extend), extends: heritageStrs(extend),
fields: fields fields: fields
}; };
Structs.push(ans) Structs.push(ans)
} }
function fromPropDecl(node: ts.PropertyDeclaration): Field { function fromPropDecl(node: ts.PropertyDeclaration): Field {
let id: ts.Identifier; let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name);
let opt = false let opt = node.questionToken != undefined;
let typ: ts.Node let typ: ts.Node = node.type;
node.forEachChild((n: ts.Node) => { const computed = computeType(typ);
if (ts.isIdentifier(n)) { let goType = computed.goType
id = n;
return
}
if (n.kind == ts.SyntaxKind.QuestionToken) {
opt = true;
return
}
if (typ != undefined)
throw new Error(`fromPropDecl too long ${loc(node)}`)
typ = n
})
let goType = computeType(typ).goType
let ans = { let ans = {
me: node, me: node,
id: id, id: id,
goName: toGoName(getText(id)), goName: toGoName(getText(id)),
optional: opt, optional: opt,
goType: goType, goType: goType,
json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\`` json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\``,
substruct: computed.fields
}; };
return ans return ans
} }
@ -199,16 +198,16 @@ function generate(files: string[], options: ts.CompilerOptions): void {
// find the Identifier from children // find the Identifier from children
// process the PropertySignature children // process the PropertySignature children
// the members might have generic info, but so do the children // the members might have generic info, but so do the children
let id: ts.Identifier; let id: ts.Identifier = node.name
let extend: ts.HeritageClause let extend: ts.HeritageClause
let generid: ts.Identifier let generid: ts.Identifier
let properties = new Array<ts.PropertySignature>() let properties = new Array<ts.PropertySignature>()
let index: ts.IndexSignatureDeclaration // generate some sort of map let index: ts.IndexSignatureDeclaration // generate some sort of map
let bad = false; // maybe we don't care about this one at all
node.forEachChild((n: ts.Node) => { node.forEachChild((n: ts.Node) => {
if (n.kind == ts.SyntaxKind.ExportKeyword || ts.isMethodSignature(n)) { if (n.kind == ts.SyntaxKind.ExportKeyword || ts.isMethodSignature(n)) {
// ignore // ignore
} else if (ts.isIdentifier(n)) { } else if (ts.isIdentifier(n)) {
id = n;
} else if (ts.isHeritageClause(n)) { } else if (ts.isHeritageClause(n)) {
extend = n; extend = n;
} else if (ts.isTypeParameterDeclaration(n)) { } else if (ts.isTypeParameterDeclaration(n)) {
@ -221,10 +220,13 @@ function generate(files: string[], options: ts.CompilerOptions): void {
throw new Error(`${loc(n)} multiple index expressions`) throw new Error(`${loc(n)} multiple index expressions`)
} }
index = n index = n
} else if (n.kind == ts.SyntaxKind.CallSignature) {
bad = true;
} else { } else {
throw new Error(`${loc(n)} doInterface ${ts.SyntaxKind[n.kind]} `) throw new Error(`${loc(n)} doInterface ${strKind(n)} `)
} }
}) })
if (bad) return;
let fields: Field[] = []; let fields: Field[] = [];
for (const p of properties) { for (const p of properties) {
fields.push(genProp(p, generid)) fields.push(genProp(p, generid))
@ -235,7 +237,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
const ans = { const ans = {
me: node, me: node,
name: toGoName(getText(id)), name: toGoName(getText(id)),
embeds: heritageStrs(extend), extends: heritageStrs(extend),
fields: fields fields: fields
}; };
@ -287,7 +289,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
} }
}) })
let goName = toGoName(id.text) let goName = toGoName(id.text)
let { goType, gostuff, optional, fields } = computeType(thing) let {goType, gostuff, optional, fields} = computeType(thing)
// Generics // Generics
if (gen && gen.text == goType) goType = 'interface{}'; if (gen && gen.text == goType) goType = 'interface{}';
opt = opt || optional; opt = opt || optional;
@ -301,11 +303,12 @@ function generate(files: string[], options: ts.CompilerOptions): void {
substruct: fields, substruct: fields,
json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\`` json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\``
}; };
// Rather than checking that goName is a const type, just do // These are not structs, so '*' would be wrong.
switch (goType) { switch (goType) {
case 'CompletionItemKind': case 'CompletionItemKind':
case 'TextDocumentSyncKind': case 'TextDocumentSyncKind':
case 'CodeActionKind': case 'CodeActionKind':
case 'FailureHandlingKind': // string
case 'InsertTextFormat': // float64 case 'InsertTextFormat': // float64
case 'DiagnosticSeverity': case 'DiagnosticSeverity':
ans.optional = false ans.optional = false
@ -315,18 +318,8 @@ function generate(files: string[], options: ts.CompilerOptions): void {
function doModuleDeclaration(node: ts.ModuleDeclaration) { function doModuleDeclaration(node: ts.ModuleDeclaration) {
// Export Identifier ModuleBlock // Export Identifier ModuleBlock
let id: ts.Identifier; let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name);
let mb: ts.ModuleBlock;
node.forEachChild((n: ts.Node) => {
if ((ts.isIdentifier(n) && (id = n)) ||
(ts.isModuleBlock(n) && mb === undefined && (mb = n)) ||
(n.kind == ts.SyntaxKind.ExportKeyword)) {
return;
}
throw new Error(`doModuleDecl ${loc(n)} ${ts.SyntaxKind[n.kind]}`)
})
// Don't want FunctionDeclarations // Don't want FunctionDeclarations
// mb has VariableStatement and useless TypeAliasDeclaration
// some of the VariableStatement are consts, and want their comments // some of the VariableStatement are consts, and want their comments
// and each VariableStatement is Export, VariableDeclarationList // and each VariableStatement is Export, VariableDeclarationList
// and each VariableDeclarationList is a single VariableDeclaration // and each VariableDeclarationList is a single VariableDeclaration
@ -352,15 +345,13 @@ function generate(files: string[], options: ts.CompilerOptions): void {
function buildConst(tname: string, node: ts.VariableDeclaration): Const { function buildConst(tname: string, node: ts.VariableDeclaration): Const {
// node is Identifier, optional-goo, (FirstLiteralToken|StringLiteral) // node is Identifier, optional-goo, (FirstLiteralToken|StringLiteral)
let id: ts.Identifier let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name);
let str: string let str: string
let first: string let first: string
node.forEachChild((n: ts.Node) => { node.forEachChild((n: ts.Node) => {
if (ts.isIdentifier(n)) { if (ts.isStringLiteral(n)) {
id = n
} else if (ts.isStringLiteral(n)) {
str = getText(n) str = getText(n)
} else if (n.kind == ts.SyntaxKind.FirstLiteralToken) { } else if (n.kind == ts.SyntaxKind.NumericLiteral) {
first = getText(n) first = getText(n)
} }
}) })
@ -393,17 +384,9 @@ function generate(files: string[], options: ts.CompilerOptions): void {
function doEnumDecl(node: ts.EnumDeclaration) { function doEnumDecl(node: ts.EnumDeclaration) {
// Generates Consts. Identifier EnumMember+ // Generates Consts. Identifier EnumMember+
// EnumMember: Identifier StringLiteral // EnumMember: Identifier StringLiteral
let id: ts.Identifier let id: ts.Identifier = node.name
let mems: ts.EnumMember[] = [] let mems = node.members
node.forEachChild((n: ts.Node) => { let theType = 'string';
if (ts.isIdentifier(n)) {
id = n // check for uniqueness?
} else if (ts.isEnumMember(n)) {
mems.push(n)
} else if (n.kind != ts.SyntaxKind.ExportKeyword) {
throw new Error(`doEnumDecl ${ts.SyntaxKind[n.kind]} ${loc(n)}`)
}
})
for (const m of mems) { for (const m of mems) {
let name: string let name: string
let value: string let value: string
@ -412,13 +395,16 @@ function generate(files: string[], options: ts.CompilerOptions): void {
name = getText(n) name = getText(n)
} else if (ts.isStringLiteral(n)) { } else if (ts.isStringLiteral(n)) {
value = getText(n).replace(/'/g, '"') value = getText(n).replace(/'/g, '"')
} else if (ts.isNumericLiteral(n)) {
value = getText(n);
theType = 'float64';
} else { } else {
throw new Error(`in doEnumDecl ${ts.SyntaxKind[n.kind]} ${loc(n)}`) throw new Error(`in doEnumDecl ${strKind(n)} ${loc(n)}`)
} }
}) })
let ans = { let ans = {
typeName: getText(id), typeName: getText(id),
goType: 'string', goType: theType,
me: m, me: m,
name: name, name: name,
value: value value: value
@ -430,31 +416,20 @@ function generate(files: string[], options: ts.CompilerOptions): void {
// top-level TypeAlias // top-level TypeAlias
function doTypeAlias(node: ts.TypeAliasDeclaration) { function doTypeAlias(node: ts.TypeAliasDeclaration) {
// these are all Export Identifier alias // these are all Export Identifier alias
let id: ts.Identifier; let id: ts.Identifier = node.name;
let alias: ts.Node; let alias: ts.Node = node.type;
let genid: ts.TypeParameterDeclaration // <T>, but we don't care
node.forEachChild((n: ts.Node) => {
if ((ts.isIdentifier(n) && (id = n)) ||
(n.kind == ts.SyntaxKind.ExportKeyword) ||
ts.isTypeParameterDeclaration(n) && (genid = n) ||
(alias === undefined && (alias = n))) {
return
}
throw new Error(`doTypeAlias ${loc(n)} ${ts.SyntaxKind[n.kind]}`)
})
let ans = { let ans = {
me: node, me: node,
id: id, id: id,
goName: toGoName(getText(id)), goName: toGoName(getText(id)),
goType: '?', goType: '?', // filled in later in this function
stuff: '' stuff: ''
}; };
if (id.text.indexOf('--') != -1) {
return
} // don't care
if (ts.isUnionTypeNode(alias)) { if (ts.isUnionTypeNode(alias)) {
ans.goType = weirdUnionType(alias) ans.goType = weirdUnionType(alias)
if (ans.goType == undefined) { // these are redundant if (id.text == 'DocumentFilter')
if (ans.goType == undefined) {
// these are mostly redundant; maybe sort them out later
return return
} }
Types.push(ans) Types.push(ans)
@ -464,11 +439,14 @@ function generate(files: string[], options: ts.CompilerOptions): void {
let embeds: string[] = [] let embeds: string[] = []
alias.forEachChild((n: ts.Node) => { alias.forEachChild((n: ts.Node) => {
if (ts.isTypeReferenceNode(n)) { if (ts.isTypeReferenceNode(n)) {
embeds.push(toGoName(computeType(n).goType)) const s = toGoName(computeType(n).goType)
embeds.push(s)
// It's here just for embedding, and not used independently
dontEmit.set(s, true);
} else } else
throw new Error(`expected TypeRef ${ts.SyntaxKind[n.kind]} ${loc(n)}`) 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) Structs.push(ans)
return return
} }
@ -480,6 +458,9 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (ts.isLiteralTypeNode(alias)) { if (ts.isLiteralTypeNode(alias)) {
return // type A = 1, so nope return // type A = 1, so nope
} }
if (ts.isTypeLiteralNode(alias)) {
return; // type A = {...}
}
if (ts.isTypeReferenceNode(alias)) { if (ts.isTypeReferenceNode(alias)) {
ans.goType = computeType(alias).goType ans.goType = computeType(alias).goType
if (ans.goType.match(/und/) != null) throw new Error('396') if (ans.goType.match(/und/) != null) throw new Error('396')
@ -491,21 +472,36 @@ function generate(files: string[], options: ts.CompilerOptions): void {
Types.push(ans); Types.push(ans);
return return
} }
throw new Error(`in doTypeAlias ${loc(node)} ${kinds(node)} ${ throw new Error(
ts.SyntaxKind[alias.kind]}\n`) `in doTypeAlias ${loc(alias)} ${kinds(node)}: ${strKind(alias)}\n`)
} }
// extract the one useful but weird case () // string, or number, or DocumentFilter
function weirdUnionType(node: ts.UnionTypeNode): string { function weirdUnionType(node: ts.UnionTypeNode): string {
let bad = false let bad = false;
let aNumber = false;
let aString = false;
let tl: ts.TypeLiteralNode[] = [] let tl: ts.TypeLiteralNode[] = []
node.forEachChild((n: ts.Node) => { node.forEachChild((n: ts.Node) => {
if (ts.isTypeLiteralNode(n)) { if (ts.isTypeLiteralNode(n)) {
tl.push(n) tl.push(n);
} else return;
}
if (ts.isLiteralTypeNode(n)) {
n.literal.kind == ts.SyntaxKind.NumericLiteral ? aNumber = true :
aString = true;
return;
}
bad = true bad = true
}) })
if (bad) return // none of these are useful (so far) 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)}`);
return 'float64';
}
if (aString) return 'string';
let x = computeType(tl[0]) let x = computeType(tl[0])
x.fields[0].json = x.fields[0].json.replace(/"`/, ',omitempty"`') x.fields[0].json = x.fields[0].json.replace(/"`/, ',omitempty"`')
let out: string[] = []; let out: string[] = [];
@ -517,24 +513,25 @@ function generate(files: string[], options: ts.CompilerOptions): void {
return ans return ans
} }
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) { switch (node.kind) {
case ts.SyntaxKind.AnyKeyword: case ts.SyntaxKind.AnyKeyword:
case ts.SyntaxKind.ObjectKeyword: case ts.SyntaxKind.ObjectKeyword:
return { goType: 'interface{}' }; return {goType: 'interface{}'};
case ts.SyntaxKind.BooleanKeyword: case ts.SyntaxKind.BooleanKeyword:
return { goType: 'bool' }; return {goType: 'bool'};
case ts.SyntaxKind.NumberKeyword: case ts.SyntaxKind.NumberKeyword:
return { goType: 'float64' }; return {goType: 'float64'};
case ts.SyntaxKind.StringKeyword: case ts.SyntaxKind.StringKeyword:
return { goType: 'string' }; return {goType: 'string'};
case ts.SyntaxKind.NullKeyword: case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.UndefinedKeyword: case ts.SyntaxKind.UndefinedKeyword:
return { goType: 'nil' }; return {goType: 'nil'};
} }
if (ts.isArrayTypeNode(node)) { if (ts.isArrayTypeNode(node)) {
let { goType, gostuff, optional } = computeType(node.elementType) let {goType, gostuff, optional} = computeType(node.elementType)
return ({ goType: '[]' + goType, gostuff: gostuff, optional: optional }) return ({goType: '[]' + goType, gostuff: gostuff, optional: optional})
} else if (ts.isTypeReferenceNode(node)) { } else if (ts.isTypeReferenceNode(node)) {
// typeArguments?: NodeArray<TypeNode>;typeName: EntityName; // typeArguments?: NodeArray<TypeNode>;typeName: EntityName;
// typeArguments won't show up in the generated Go // typeArguments won't show up in the generated Go
@ -543,10 +540,10 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (ts.isQualifiedName(tn)) { if (ts.isQualifiedName(tn)) {
throw new Error(`qualified name at ${loc(node)}`); throw new Error(`qualified name at ${loc(node)}`);
} else if (ts.isIdentifier(tn)) { } else if (ts.isIdentifier(tn)) {
return { goType: tn.text }; return {goType: tn.text};
} else { } else {
throw new Error(`expected identifier got ${ throw new Error(
ts.SyntaxKind[node.typeName.kind]} at ${loc(tn)}`) `expected identifier got ${strKind(node.typeName)} at ${loc(tn)}`)
} }
} else if (ts.isLiteralTypeNode(node)) { } else if (ts.isLiteralTypeNode(node)) {
// string|float64 (are there other possibilities?) // string|float64 (are there other possibilities?)
@ -555,7 +552,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (txt.charAt(0) == '\'') { if (txt.charAt(0) == '\'') {
typ = 'string' typ = 'string'
} }
return { goType: typ, gostuff: getText(node) }; return {goType: typ, gostuff: getText(node)};
} else if (ts.isTypeLiteralNode(node)) { } else if (ts.isTypeLiteralNode(node)) {
let x: Field[] = []; let x: Field[] = [];
let indexCnt = 0 let indexCnt = 0
@ -568,32 +565,34 @@ function generate(files: string[], options: ts.CompilerOptions): void {
x.push(fromIndexSignature(n)) x.push(fromIndexSignature(n))
return return
} }
throw new Error( throw new Error(`${loc(n)} gotype ${strKind(n)}, not expected`)
`${loc(n)} gotype ${ts.SyntaxKind[n.kind]}, not expected`)
}); });
if (indexCnt > 0) { if (indexCnt > 0) {
if (indexCnt != 1 || x.length != 1) if (indexCnt != 1 || x.length != 1)
throw new Error(`undexpected Index ${loc(x[0].me)}`) throw new Error(`undexpected Index ${loc(x[0].me)}`)
// instead of {map...} just the map // 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)) { } else if (ts.isUnionTypeNode(node)) {
let x = new Array<{ goType: string, gostuff?: string, optiona?: boolean }>() let x = new Array<{goType: string, gostuff?: string, optiona?: boolean}>()
node.forEachChild((n: ts.Node) => { x.push(computeType(n)) }) node.forEachChild((n: ts.Node) => {x.push(computeType(n))})
if (x.length == 2 && x[1].goType == 'nil') { if (x.length == 2 && x[1].goType == 'nil') {
return x[0] // make it optional somehow? TODO return x[0] // make it optional somehow? TODO
} }
if (x[0].goType == 'bool') { // take it if (x[0].goType == 'bool') { // take it
return ({ goType: 'bool', gostuff: getText(node) }) if (x[1].goType == 'RenameOptions') {
return ({goType: 'RenameOptions', gostuff: getText(node)})
}
return ({goType: 'bool', gostuff: getText(node)})
} }
// these are special cases from looking at the source // these are special cases from looking at the source
let gostuff = getText(node); let gostuff = getText(node);
if (x[0].goType == `"off"` || x[0].goType == 'string') { if (x[0].goType == `"off"` || x[0].goType == 'string') {
return ({ goType: 'string', gostuff: gostuff }) return ({goType: 'string', gostuff: gostuff})
} }
if (x[0].goType == 'TextDocumentSyncOptions') { if (x[0].goType == 'TextDocumentSyncOptions') {
return ({ goType: 'interface{}', gostuff: gostuff }) return ({goType: 'interface{}', gostuff: gostuff})
} }
if (x[0].goType == 'float64' && x[1].goType == 'string') { if (x[0].goType == 'float64' && x[1].goType == 'string') {
return { return {
@ -605,6 +604,11 @@ function generate(files: string[], options: ts.CompilerOptions): void {
goType: 'MarkupContent', gostuff: gostuff goType: 'MarkupContent', gostuff: gostuff
} }
} }
if (x[0].goType == 'RequestMessage' && x[1].goType == 'ResponseMessage') {
return {
goType: 'interface{}', gostuff: gostuff
}
}
// Fail loudly // Fail loudly
console.log(`UnionType ${loc(node)}`) console.log(`UnionType ${loc(node)}`)
for (const v of x) { for (const v of x) {
@ -613,17 +617,22 @@ function generate(files: string[], options: ts.CompilerOptions): void {
throw new Error('in UnionType, weird') throw new Error('in UnionType, weird')
} else if (ts.isParenthesizedTypeNode(node)) { } else if (ts.isParenthesizedTypeNode(node)) {
// check that this is (TextDocumentEdit | CreateFile | RenameFile | // check that this is (TextDocumentEdit | CreateFile | RenameFile |
// DeleteFile) // DeleteFile) TODO(pjw)
return { return {
goType: 'TextDocumentEdit', gostuff: getText(node) goType: 'TextDocumentEdit', gostuff: getText(node)
} }
} else if (ts.isTupleTypeNode(node)) { } else if (ts.isTupleTypeNode(node)) {
// string | [number, number] // string | [number, number]. TODO(pjw): check it really is
return { return {
goType: 'string', gostuff: getText(node) goType: 'string', gostuff: getText(node)
} }
} else if (ts.isFunctionTypeNode(node)) {
// we don't want these members; mark them
return {
goType: 'bad', gostuff: getText(node)
} }
throw new Error(`unknown ${ts.SyntaxKind[node.kind]} at ${loc(node)}`) }
throw new Error(`computeType unknown ${strKind(node)} at ${loc(node)}`)
} }
function fromIndexSignature(node: ts.IndexSignatureDeclaration): Field { function fromIndexSignature(node: ts.IndexSignatureDeclaration): Field {
@ -637,7 +646,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
ts.isUnionTypeNode(n)) { ts.isUnionTypeNode(n)) {
at = n at = n
} else } else
throw new Error(`fromIndexSig ${ts.SyntaxKind[n.kind]} ${loc(n)}`) throw new Error(`fromIndexSig ${strKind(n)} ${loc(n)}`)
}) })
let goType = computeType(at).goType let goType = computeType(at).goType
let id: ts.Identifier let id: ts.Identifier
@ -645,8 +654,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (ts.isIdentifier(n)) { if (ts.isIdentifier(n)) {
id = n id = n
} else if (n.kind != ts.SyntaxKind.StringKeyword) { } else if (n.kind != ts.SyntaxKind.StringKeyword) {
throw new Error( throw new Error(`fromIndexSig expected string, ${strKind(n)} ${loc(n)}`)
`fromIndexSig expected string, ${ts.SyntaxKind[n.kind]} ${loc(n)}`)
} }
}) })
goType = `map[string]${goType}` goType = `map[string]${goType}`
@ -662,13 +670,12 @@ function generate(files: string[], options: ts.CompilerOptions): void {
if (s.charAt(0) == '_') { if (s.charAt(0) == '_') {
ans = 'Inner' + s.substring(1) 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(/Uri$/, 'URI')
ans = ans.replace(/Id$/, 'ID') ans = ans.replace(/Id$/, 'ID')
return ans return ans
} }
// find the text of a node // find the text of a node
function getText(node: ts.Node): string { function getText(node: ts.Node): string {
let sf = node.getSourceFile(); let sf = node.getSourceFile();
@ -678,12 +685,39 @@ function generate(files: string[], options: ts.CompilerOptions): void {
} }
// return a string of the kinds of the immediate descendants // return a string of the kinds of the immediate descendants
function kinds(n: ts.Node): string { function kinds(n: ts.Node): string {
let res = 'Seen ' + ts.SyntaxKind[n.kind]; let res = 'Seen ' + strKind(n);
function f(n: ts.Node): void { res += ' ' + ts.SyntaxKind[n.kind] }; function f(n: ts.Node): void{res += ' ' + strKind(n)};
ts.forEachChild(n, f) ts.forEachChild(n, f)
return res return res
} }
function strKind(n: ts.Node): string {
const x = ts.SyntaxKind[n.kind];
// some of these have two names
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) { function describe(node: ts.Node) {
if (node === undefined) { if (node === undefined) {
return return
@ -693,36 +727,22 @@ function generate(files: string[], options: ts.CompilerOptions): void {
function f(n: ts.Node) { function f(n: ts.Node) {
seenAdd(kinds(n)) seenAdd(kinds(n))
if (ts.isIdentifier(n)) { if (ts.isIdentifier(n)) {
pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]} ${n.text}\n`) pra(`${indent} ${loc(n)} ${strKind(n)} ${n.text}\n`)
} }
else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) { else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) {
pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]}\n`) pra(`${indent} ${loc(n)} ${strKind(n)}\n`)
} }
else if (ts.isTypeLiteralNode(n)) { else if (ts.isTypeLiteralNode(n)) {
let m = n.members let m = n.members
pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]} ${m.length}\n`) pra(`${indent} ${loc(n)} ${strKind(n)} ${m.length}\n`)
} }
else { pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]}\n`) }; else {pra(`${indent} ${loc(n)} ${strKind(n)}\n`)};
indent += ' ' indent += ' '
ts.forEachChild(n, f) ts.forEachChild(n, f)
indent = indent.slice(0, indent.length - 2) indent = indent.slice(0, indent.length - 2)
} }
f(node) f(node)
} }
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})`
}
} }
function getComments(node: ts.Node): string { function getComments(node: ts.Node): string {
@ -733,55 +753,134 @@ function getComments(node: ts.Node): string {
return x 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)
return `${fn} ${x.line + 1}:${x.character + 1} (${y.line + 1}:${
y.character + 1})`
}
function emitTypes() { function emitTypes() {
seenConstTypes.set('MessageQueue', true); // skip
for (const t of Types) { for (const t of Types) {
if (seenConstTypes.get(t.goName)) continue;
if (t.goName == 'CodeActionKind') continue; // consts better choice if (t.goName == 'CodeActionKind') continue; // consts better choice
if (t.goType === undefined) continue;
let stuff = (t.stuff == undefined) ? '' : t.stuff; let stuff = (t.stuff == undefined) ? '' : t.stuff;
prgo(`// ${t.goName} is a type\n`) prgo(`// ${t.goName} is a type\n`)
prgo(`${getComments(t.me)}`) 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);
} }
} }
function emitStructs() { let byName = new Map<string, Struct>();
function emitStructs() {
dontEmit.set('Thenable', true);
dontEmit.set('EmitterOptions', true);
dontEmit.set('MessageReader', true);
dontEmit.set('MessageWriter', true);
dontEmit.set('CancellationToken', true);
dontEmit.set('PipeTransport', true);
dontEmit.set('SocketTransport', true);
dontEmit.set('Item', true);
dontEmit.set('Event', true);
dontEmit.set('Logger', true);
dontEmit.set('Disposable', true);
dontEmit.set('PartialMessageInfo', true);
dontEmit.set('MessageConnection', true);
dontEmit.set('ResponsePromise', true);
dontEmit.set('ResponseMessage', true);
dontEmit.set('ErrorMessage', true);
dontEmit.set('NotificationMessage', true);
dontEmit.set('RequestHandlerElement', true);
dontEmit.set('RequestMessage', true);
dontEmit.set('NotificationHandlerElement', true);
dontEmit.set('Message', true); // duplicate of jsonrpc2:wire.go
dontEmit.set('LSPLogMessage', true);
dontEmit.set('InnerEM', true);
dontEmit.set('ResponseErrorLiteral', true);
dontEmit.set('TraceOptions', true);
dontEmit.set('MessageType', true); // want the enum
// backwards compatibility, done in requests.ts:
dontEmit.set('CancelParams', true);
for (const str of Structs) {
byName.set(str.name, str)
}
let seenName = new Map<string, boolean>() let seenName = new Map<string, boolean>()
for (const str of Structs) { for (const str of Structs) {
if (str.name == 'InitializeError') { if (str.name == 'InitializeError') {
// only want the consts // only want its consts, not the struct
continue continue
} }
if (seenName[str.name]) { if (seenName.get(str.name) || dontEmit.get(str.name)) {
continue continue
} }
seenName[str.name] = true let noopt = false;
seenName.set(str.name, true)
prgo(genComments(str.name, getComments(str.me))) prgo(genComments(str.name, getComments(str.me)))
/* prgo(`// ${str.name} is:\n`)
prgo(getComments(str.me))*/
prgo(`type ${str.name} struct {\n`) prgo(`type ${str.name} struct {\n`)
for (const s of str.embeds) { // if it has fields, generate them
prgo(`\t${s}\n`)
}
if (str.fields != undefined) { if (str.fields != undefined) {
for (const f of str.fields) { for (const f of str.fields) {
prgo(strField(f)) prgo(strField(f, noopt))
} }
} }
prgo(`}\n`) if (str.extends) {
// ResourceOperation just repeats the Kind field
for (const s of str.extends) {
if (s != 'ResourceOperation')
prgo(`\t${s}\n`) // what this type extends.
}
} else if (str.embeds) {
prb(`embeds: ${str.name}\n`);
noopt = (str.name == 'ClientCapabilities');
// embedded struct. the hard case is from intersection types,
// where fields with the same name have to be combined into
// a single struct
let fields = new Map<string, Field[]>();
for (const e of str.embeds) {
const nm = byName.get(e);
if (nm.embeds) throw new Error(`${nm.name} is an embedded embed`);
// each of these fields might be a something that needs amalgamating
for (const f of nm.fields) {
let x = fields.get(f.goName);
if (x === undefined) x = [];
x.push(f);
fields.set(f.goName, x);
}
}
fields.forEach((val) => {
if (val.length > 1) {
// merge the fields with the same name
prgo(strField(val[0], noopt, val));
} else {
prgo(strField(val[0], noopt));
}
});
}
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 == '') return `\n\t// ${name} is\n`;
if (maybe.indexOf('/**') == 0) { if (maybe.indexOf('/**') == 0) {
return maybe.replace('/**', `\n/*${name} defined:`) return maybe.replace('/**', `\n/*${name} defined:`)
} }
throw new Error(`weird comment ${maybe.indexOf('/**')}`) throw new Error(`weird comment ${maybe.indexOf('/**')}`)
} }
// Turn a Field into an output string // Turn a Field into an output string
function strField(f: Field): string { function strField(f: Field, noopt?: boolean, flds?: Field[]): string {
let ans: string[] = []; let ans: string[] = [];
let opt = f.optional ? '*' : '' let opt = (!noopt && f.optional) ? '*' : ''
switch (f.goType.charAt(0)) { switch (f.goType.charAt(0)) {
case 's': // string case 's': // string
case 'b': // bool case 'b': // bool
@ -792,33 +891,51 @@ function strField(f: Field): string {
} }
let stuff = (f.gostuff == undefined) ? '' : ` // ${f.gostuff}` let stuff = (f.gostuff == undefined) ? '' : ` // ${f.gostuff}`
ans.push(genComments(f.goName, getComments(f.me))) ans.push(genComments(f.goName, getComments(f.me)))
if (f.substruct == undefined) { if (flds === undefined && f.substruct == undefined) {
ans.push(`\t${f.goName} ${opt}${f.goType} ${f.json}${stuff}\n`) ans.push(`\t${f.goName} ${opt}${f.goType} ${f.json}${stuff}\n`)
} }
else if (flds !== undefined) {
ans.push(`\t${f.goName} ${opt}struct{\n`);
for (const ff of flds) {
if (ff.substruct !== undefined) {
for (const x of ff.substruct) {
ans.push(strField(x, noopt))
}
} else if (byName.get(ff.goType) !== undefined) {
const st = byName.get(ff.goType);
for (let i = 0; i < st.fields.length; i++) {
ans.push(strField(st.fields[i], noopt))
}
} else {
ans.push(strField(ff, noopt));
}
}
ans.push(`\t} ${f.json}${stuff}\n`);
}
else { else {
ans.push(`\t${f.goName} ${opt}struct {\n`) ans.push(`\t${f.goName} ${opt}struct {\n`)
for (const x of f.substruct) { for (const x of f.substruct) {
ans.push(strField(x)) ans.push(strField(x, noopt))
} }
ans.push(`\t} ${f.json}${stuff}\n`) ans.push(`\t} ${f.json}${stuff}\n`)
} }
return (''.concat(...ans)) return (''.concat(...ans))
} }
function emitConsts() { function emitConsts() {
// Generate modifying prefixes and suffixes to ensure consts are // need the consts too! Generate modifying prefixes and suffixes to ensure
// unique. (Go consts are package-level, but Typescript's are not.) // consts are unique. (Go consts are package-level, but Typescript's are
// Use suffixes to minimize changes to gopls. // not.) Use suffixes to minimize changes to gopls.
let pref = new Map<string, string>( let pref = new Map<string, string>(
[['DiagnosticSeverity', 'Severity']]) // typeName->prefix [['DiagnosticSeverity', 'Severity']]) // typeName->prefix
let suff = new Map<string, string>([ let suff = new Map<string, string>([
['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'] ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat']
]) ])
for (const c of Consts) { for (const c of Consts) {
if (seenConstTypes[c.typeName]) { if (seenConstTypes.get(c.typeName)) {
continue continue
} }
seenConstTypes[c.typeName] = true seenConstTypes.set(c.typeName, true);
if (pref.get(c.typeName) == undefined) { if (pref.get(c.typeName) == undefined) {
pref.set(c.typeName, '') // initialize to empty value pref.set(c.typeName, '') // initialize to empty value
} }
@ -836,13 +953,15 @@ function emitConsts() {
continue continue
} }
seenConsts.set(x, true) seenConsts.set(x, true)
if (c.value === undefined) continue; // didn't figure it out
if (x.startsWith('undefined')) continue; // what's going on here?
prgo(genComments(x, getComments(c.me))) prgo(genComments(x, getComments(c.me)))
prgo(`\t${x} ${c.typeName} = ${c.value}\n`) prgo(`\t${x} ${c.typeName} = ${c.value}\n`)
} }
prgo(')\n') prgo(')\n')
} }
function emitHeader(files: string[]) { function emitHeader(files: string[]) {
let lastMod = 0 let lastMod = 0
let lastDate: Date let lastDate: Date
for (const f of files) { for (const f of files) {
@ -852,14 +971,17 @@ function emitHeader(files: string[]) {
lastDate = st.mtime lastDate = st.mtime
} }
} }
prgo(`// Package protocol contains data types for LSP jsonrpcs\n`) let a = fs.readFileSync(`${dir}${srcDir}/.git/refs/heads/master`);
prgo(`// generated automatically from vscode-languageserver-node prgo(`// Package protocol contains data types and code for LSP jsonrpcs\n`)
// version of ${lastDate}\n`) prgo(`// generated automatically from vscode-languageserver-node\n`)
prgo(`// commit: ${a.toString()}`)
prgo(`// last fetched ${lastDate}\n`)
prgo('package protocol\n\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 // ad hoc argument parsing: [-d dir] [-o outputfile], and order matters
function main() { function main() {
let args = process.argv.slice(2) // effective command line let args = process.argv.slice(2) // effective command line
if (args.length > 0) { if (args.length > 0) {
let j = 0; let j = 0;
@ -879,11 +1001,11 @@ function main() {
} }
createOutputFiles() createOutputFiles()
generate( generate(
files, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }); files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS});
emitHeader(files) emitHeader(files)
emitStructs() emitStructs()
emitConsts() emitConsts()
emitTypes() emitTypes()
} }
main() main()

View File

@ -0,0 +1,472 @@
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. serverHandler(...) { return func(...) { 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 {
conn.Reply(ctx, r, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params"))
return
}`;
// 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 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, log, conn, r, err)
return
}
if err := ${side.name}.${nm}(ctx, &params); err != nil {
log.Errorf(ctx, "%v", err)
}`;
} else {
case1 = `if err := ${side.name}.${nm}(ctx); err != nil {
log.Errorf(ctx, "%v", err)
}`;
}
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(m, n.typeArguments[0]);
let b = goType(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, log, conn, r, err)
return
}`;
}
const arg2 = a == '' ? '' : ', &params';
let case2 = `if err := ${side.name}.${nm}(ctx${arg2}); err != nil {
log.Errorf(ctx, "%v", err)
}`;
if (b != '') {
case2 = `resp, err := ${side.name}.${nm}(ctx${arg2})
if err := conn.Reply(ctx, r, resp, err); err != nil {
log.Errorf(ctx, "%v", err)
}`;
} else { // response is nil
case2 = `err := ${side.name}.${nm}(ctx${arg2})
if err := conn.Reply(ctx, r, nil, err); err != nil {
log.Errorf(ctx, "%v", err)
}`
}
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/lsp/xlog"
)
`);
const a = side.name[0].toUpperCase() + side.name.substring(1)
f(`type ${a} interface {`);
side.methods.forEach((v) => {f(v)});
f('}\n');
f(`func ${side.name}Handler(log xlog.Logger, ${side.name} ${
side.goName}) jsonrpc2.Handler {
return func(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) {
switch r.Method {
case "$/cancelRequest":
var params CancelParams
if err := json.Unmarshal(*r.Params, &params); err != nil {
sendParseError(ctx, log, conn, r, err)
return
}
conn.Cancel(params.ID)`);
side.cases.forEach((v) => {f(v)});
f(`
default:
if r.IsNotify() {
conn.Reply(ctx, r, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not found", r.Method))
}
}
}
}`);
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"\`
}`);
}
interface side {
methods: string[];
cases: string[];
calls: 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'};
let server: side =
{methods: [], cases: [], calls: [], name: 'server', goName: 'Server'};
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(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);
// 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(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`)
}
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);
}
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()