diff options
Diffstat (limited to 'internal/lsp/protocol/typescript/code.ts')
-rw-r--r-- | internal/lsp/protocol/typescript/code.ts | 1448 |
1 files changed, 0 insertions, 1448 deletions
diff --git a/internal/lsp/protocol/typescript/code.ts b/internal/lsp/protocol/typescript/code.ts deleted file mode 100644 index dcb1b67a5..000000000 --- a/internal/lsp/protocol/typescript/code.ts +++ /dev/null @@ -1,1448 +0,0 @@ -/* eslint-disable no-useless-return */ -// read files from vscode-languageserver-node, and generate Go rpc stubs -// and data definitions. (and maybe someday unmarshaling code) - -// The output is 3 files, tsprotocol.go contains the type definitions -// while tsclient.go and tsserver.go contain the LSP API and stub. An LSP server -// uses both APIs. To read the code, start in this file's main() function. - -// The code is rich in heuristics and special cases, some of which are to avoid -// extensive changes to gopls, and some of which are due to the mismatch between -// typescript and Go types. In particular, there is no Go equivalent to union -// types, so each case ought to be considered separately. The Go equivalent of A -// & B could frequently be struct{A;B;}, or it could be the equivalent type -// listing all the members of A and B. Typically the code uses the former, but -// especially if A and B have elements with the same name, it does a version of -// the latter. ClientCapabilities has to be expanded, and ServerCapabilities is -// expanded to make the generated code easier to read. - -// for us typescript ignorati, having an import makes this file a module -import * as fs from 'fs'; -import * as ts from 'typescript'; -import * as u from './util'; -import { constName, getComments, goName, loc, strKind } from './util'; - -var program: ts.Program; - -function parse() { - // this won't complain if some fnames don't exist - program = ts.createProgram( - u.fnames, - { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS }); - program.getTypeChecker(); // finish type checking and assignment -} - -// ----- collecting information for RPCs -let req = new Map<string, ts.NewExpression>(); // requests -let not = new Map<string, ts.NewExpression>(); // notifications -let ptypes = new Map<string, [ts.TypeNode, ts.TypeNode]>(); // req, resp types -let receives = new Map<string, 'server' | 'client'>(); // who receives it -let rpcTypes = new Set<string>(); // types seen in the rpcs - -function findRPCs(node: ts.Node) { - if (!ts.isModuleDeclaration(node)) { - return; - } - if (!ts.isIdentifier(node.name)) { - throw new Error( - `expected Identifier, got ${strKind(node.name)} at ${loc(node)}`); - } - let reqnot = req; - let v = node.name.getText(); - if (v.endsWith('Notification')) reqnot = not; - else if (!v.endsWith('Request')) return; - - if (!ts.isModuleBlock(node.body)) { - throw new Error( - `expected ModuleBlock got ${strKind(node.body)} at ${loc(node)}`); - } - let x: ts.ModuleBlock = node.body; - // The story is to expect const method = 'textDocument/implementation' - // const type = new ProtocolRequestType<...>(method) - // but the method may be an explicit string - let rpc: string = ''; - let newNode: ts.NewExpression; - for (let i = 0; i < x.statements.length; i++) { - const uu = x.statements[i]; - if (!ts.isVariableStatement(uu)) continue; - const dl: ts.VariableDeclarationList = uu.declarationList; - if (dl.declarations.length != 1) - throw new Error(`expected a single decl at ${loc(dl)}`); - const decl: ts.VariableDeclaration = dl.declarations[0]; - const name = decl.name.getText(); - // we want the initializers - if (name == 'method') { // mostly StringLiteral but NoSubstitutionTemplateLiteral in protocol.semanticTokens.ts - if (!ts.isStringLiteral(decl.initializer)) { - if (!ts.isNoSubstitutionTemplateLiteral(decl.initializer)) { - console.log(`81: ${decl.initializer.getText()}`); - throw new Error(`expect StringLiteral at ${loc(decl)} got ${strKind(decl.initializer)}`); - } - } - rpc = decl.initializer.getText(); - } - else if (name == 'type') { // NewExpression - if (!ts.isNewExpression(decl.initializer)) - throw new Error(`89 expected new at ${loc(decl)}`); - const nn: ts.NewExpression = decl.initializer; - newNode = nn; - const mtd = nn.arguments[0]; - if (ts.isStringLiteral(mtd)) rpc = mtd.getText(); - switch (nn.typeArguments.length) { - case 1: // exit - ptypes.set(rpc, [nn.typeArguments[0], null]); - break; - case 2: // notifications - ptypes.set(rpc, [nn.typeArguments[0], null]); - break; - case 4: // request with no parameters - ptypes.set(rpc, [null, nn.typeArguments[0]]); - break; - case 5: // request req, resp, partial(?) - ptypes.set(rpc, [nn.typeArguments[0], nn.typeArguments[1]]); - break; - default: - throw new Error(`${nn.typeArguments?.length} at ${loc(nn)}`); - } - } - } - if (rpc == '') throw new Error(`112 no name found at ${loc(x)}`); - // remember the implied types - const [a, b] = ptypes.get(rpc); - const add = function (n: ts.Node) { - rpcTypes.add(goName(n.getText())); - }; - underlying(a, add); - underlying(b, add); - rpc = rpc.substring(1, rpc.length - 1); // 'exit' - reqnot.set(rpc, newNode); -} - -function setReceives() { - // mark them all as server, then adjust the client ones. - // it would be nice to have some independent check on this - // (this logic fails if the server ever sends $/canceRequest - // or $/progress) - req.forEach((_, k) => { receives.set(k, 'server'); }); - not.forEach((_, k) => { receives.set(k, 'server'); }); - receives.set('window/showMessage', 'client'); - receives.set('window/showMessageRequest', 'client'); - receives.set('window/logMessage', 'client'); - receives.set('telemetry/event', 'client'); - receives.set('client/registerCapability', 'client'); - receives.set('client/unregisterCapability', 'client'); - receives.set('workspace/workspaceFolders', 'client'); - receives.set('workspace/configuration', 'client'); - receives.set('workspace/applyEdit', 'client'); - receives.set('textDocument/publishDiagnostics', 'client'); - receives.set('window/workDoneProgress/create', 'client'); - receives.set('window/showDocument', 'client'); - receives.set('$/progress', 'client'); - // a small check - receives.forEach((_, k) => { - if (!req.get(k) && !not.get(k)) throw new Error(`145 missing ${k}}`); - if (req.get(k) && not.get(k)) throw new Error(`146 dup ${k}`); - }); -} - -type DataKind = 'module' | 'interface' | 'alias' | 'enum' | 'class'; - -interface Data { - kind: DataKind; - me: ts.Node; // root node for this type - name: string; // Go name - origname: string; // their name - generics: ts.NodeArray<ts.TypeParameterDeclaration>; - as: ts.NodeArray<ts.HeritageClause>; // inheritance - // Interface - properties: ts.NodeArray<ts.PropertySignature> - alias: ts.TypeNode; // type alias - // module - statements: ts.NodeArray<ts.Statement>; - enums: ts.NodeArray<ts.EnumMember>; - // class - members: ts.NodeArray<ts.PropertyDeclaration>; -} -function newData(n: ts.Node, nm: string, k: DataKind, origname: string): Data { - return { - kind: k, - me: n, name: goName(nm), origname: origname, - generics: ts.factory.createNodeArray<ts.TypeParameterDeclaration>(), - as: ts.factory.createNodeArray<ts.HeritageClause>(), - properties: ts.factory.createNodeArray<ts.PropertySignature>(), alias: undefined, - statements: ts.factory.createNodeArray<ts.Statement>(), - enums: ts.factory.createNodeArray<ts.EnumMember>(), - members: ts.factory.createNodeArray<ts.PropertyDeclaration>(), - }; -} - -// for debugging, produce a skeleton description -function strData(d: Data): string { - if (!d) { return 'nil'; } - const f = function (na: ts.NodeArray<any>): number { - return na.length; - }; - const nm = d.name == d.origname ? `${d.name}` : `${d.name}/${d.origname}`; - return `g:${f(d.generics)} a:${f(d.as)} p:${f(d.properties)} s:${f(d.statements)} e:${f(d.enums)} m:${f(d.members)} a:${d.alias !== undefined} D(${nm}) k:${d.kind}`; -} - -let data = new Map<string, Data>(); // parsed data types -let seenTypes = new Map<string, Data>(); // type names we've seen -let extraTypes = new Map<string, string[]>(); // to avoid struct params - -function setData(nm: string, d: Data) { - const v = data.get(nm); - if (!v) { - data.set(nm, d); - return; - } - // if there are multiple definitions of the same name, decide what to do. - // For now the choices are only aliases and modules - // alias is preferred unless the constant values are needed - if (nm === 'PrepareSupportDefaultBehavior') { - // want the alias, as we're going to change the type and can't afford a constant - if (d.kind === 'alias') data.set(nm, d); - else if (v.kind == 'alias') data.set(nm, v); - else throw new Error(`208 ${d.kind} ${v.kind}`); - return; - } - if (nm === 'CodeActionKind') { - // want the module, need the constants - if (d.kind === 'module') data.set(nm, d); - else if (v.kind === 'module') data.set(nm, v); - else throw new Error(`215 ${d.kind} ${v.kind}`); - } - if (v.kind === 'alias' && d.kind !== 'alias') return; - if (d.kind === 'alias' && v.kind !== 'alias') { - data.set(nm, d); - return; - } - if (v.kind === 'alias' && d.kind === 'alias') return; - // protocol/src/common/protocol.foldingRange.ts 44: 1 (39: 2) and - // types/src/main.ts 397: 1 (392: 2) - // for FoldingRangeKind - if (d.me.getText() === v.me.getText()) return; - // error messages for an unexpected case - console.log(`228 ${strData(v)} ${loc(v.me)} for`); - console.log(`229 ${v.me.getText().replace(/\n/g, '\\n')}`); - console.log(`230 ${strData(d)} ${loc(d.me)}`); - console.log(`231 ${d.me.getText().replace(/\n/g, '\\n')}`); - throw new Error(`232 setData found ${v.kind} for ${d.kind}`); -} - -// look at top level data definitions -function genTypes(node: ts.Node) { - // Ignore top-level items that can't produce output - if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) || - ts.isImportDeclaration(node) || ts.isVariableStatement(node) || - ts.isExportDeclaration(node) || ts.isEmptyStatement(node) || - ts.isExportAssignment(node) || ts.isImportEqualsDeclaration(node) || - ts.isBlock(node) || node.kind == ts.SyntaxKind.EndOfFileToken) { - return; - } - if (ts.isInterfaceDeclaration(node)) { - const v: ts.InterfaceDeclaration = node; - // need to check the members, many of which are disruptive - let mems: ts.PropertySignature[] = []; - const f = function (t: ts.TypeElement) { - if (ts.isPropertySignature(t)) { - mems.push(t); - } else if (ts.isMethodSignature(t) || ts.isCallSignatureDeclaration(t)) { - return; - } else if (ts.isIndexSignatureDeclaration(t)) { - // probably safe to ignore these - // [key: string]: boolean | number | string | undefined; - // and InitializeResult: [custom: string]: any;] - } else - throw new Error(`259 unexpected ${strKind(t)}`); - }; - v.members.forEach(f); - if (mems.length == 0 && !v.heritageClauses && - v.name.getText() != 'InitializedParams') { - return; // Don't seem to need any of these [Logger, PipTransport, ...] - } - // Found one we want - let x = newData(v, goName(v.name.getText()), 'interface', v.name.getText()); - x.properties = ts.factory.createNodeArray<ts.PropertySignature>(mems); - if (v.typeParameters) x.generics = v.typeParameters; - if (v.heritageClauses) x.as = v.heritageClauses; - if (x.generics.length > 1) { // Unneeded - // Item interface Item<K, V>... - return; - } - if (data.has(x.name)) { // modifying one we've seen - x = dataChoose(x, data.get(x.name)); - } - setData(x.name, x); - } else if (ts.isTypeAliasDeclaration(node)) { - const v: ts.TypeAliasDeclaration = node; - let x = newData(v, v.name.getText(), 'alias', v.name.getText()); - x.alias = v.type; - // if type is a union of constants, we (mostly) don't want it - // (at the top level) - // Unfortunately this is false for TraceValues - if (ts.isUnionTypeNode(v.type) && - v.type.types.every((n: ts.TypeNode) => ts.isLiteralTypeNode(n))) { - if (x.name != 'TraceValues') return; - } - if (v.typeParameters) { - x.generics = v.typeParameters; - } - if (data.has(x.name)) x = dataChoose(x, data.get(x.name)); - if (x.generics.length > 1) { - return; - } - setData(x.name, x); - } else if (ts.isModuleDeclaration(node)) { - const v: ts.ModuleDeclaration = node; - if (!ts.isModuleBlock(v.body)) { - throw new Error(`${loc(v)} not ModuleBlock, but ${strKind(v.body)}`); - } - const b: ts.ModuleBlock = v.body; - var s: ts.Statement[] = []; - // we don't want most of these - const fx = function (x: ts.Statement) { - if (ts.isFunctionDeclaration(x)) { - return; - } - if (ts.isTypeAliasDeclaration(x) || ts.isModuleDeclaration(x)) { - return; - } - if (!ts.isVariableStatement(x)) - throw new Error( - `315 expected VariableStatment ${loc(x)} ${strKind(x)} ${x.getText()}`); - if (hasNewExpression(x)) { - return; - } - s.push(x); - }; - b.statements.forEach(fx); - if (s.length == 0) { - return; - } - let m = newData(node, v.name.getText(), 'module', v.name.getText()); - m.statements = ts.factory.createNodeArray<ts.Statement>(s); - if (data.has(m.name)) m = dataChoose(m, data.get(m.name)); - setData(m.name, m); - } else if (ts.isEnumDeclaration(node)) { - const nm = node.name.getText(); - let v = newData(node, nm, 'enum', node.name.getText()); - v.enums = node.members; - if (data.has(nm)) { - v = dataChoose(v, data.get(nm)); - } - setData(nm, v); - } else if (ts.isClassDeclaration(node)) { - const v: ts.ClassDeclaration = node; - var d: ts.PropertyDeclaration[] = []; - const wanted = function (c: ts.ClassElement): string { - if (ts.isConstructorDeclaration(c)) { - return ''; - } - if (ts.isMethodDeclaration(c)) { - return ''; - } - if (ts.isGetAccessor(c)) { - return ''; - } - if (ts.isSetAccessor(c)) { - return ''; - } - if (ts.isPropertyDeclaration(c)) { - d.push(c); - return strKind(c); - } - throw new Error(`Class decl ${strKind(c)} `); - }; - v.members.forEach((c) => wanted(c)); - if (d.length == 0) { - return; - } // don't need it - let c = newData(v, v.name.getText(), 'class', v.name.getText()); - c.members = ts.factory.createNodeArray<ts.PropertyDeclaration>(d); - if (v.typeParameters) { - c.generics = v.typeParameters; - } - if (c.generics.length > 1) { - return; - } - if (v.heritageClauses) { - c.as = v.heritageClauses; - } - if (data.has(c.name)) - throw new Error(`Class dup ${loc(c.me)} and ${loc(data.get(c.name).me)}`); - setData(c.name, c); - } else { - throw new Error(`378 unexpected ${strKind(node)} ${loc(node)} `); - } -} - -// Typescript can accumulate, but this chooses one or the other -function dataChoose(a: Data, b: Data): Data { - // maybe they are textually identical? (e.g., FoldingRangeKind) - const [at, bt] = [a.me.getText(), b.me.getText()]; - if (at == bt) { - return a; - } - switch (a.name) { - case 'InitializeError': - case 'CompletionItemTag': - case 'SymbolTag': - case 'CodeActionKind': - case 'Integer': - case 'Uinteger': - case 'Decimal': - // want the Module, if anything - return a.statements.length > 0 ? a : b; - case 'CancellationToken': - case 'CancellationStrategy': - // want the Interface - return a.properties.length > 0 ? a : b; - case 'TextDocumentContentChangeEvent': // almost the same - case 'TokenFormat': - case 'PrepareSupportDefaultBehavior': - return a; - } - console.log( - `409 ${strKind(a.me)} ${strKind(b.me)} ${a.name} ${loc(a.me)} ${loc(b.me)}`); - throw new Error(`410 Fix dataChoose for ${a.name}`); -} - -// is a node an ancestor of a NewExpression -function hasNewExpression(n: ts.Node): boolean { - let ans = false; - n.forEachChild((n: ts.Node) => { - if (ts.isNewExpression(n)) ans = true; - }); - return ans; -} - -function checkOnce() { - // Data for all the rpc types? - rpcTypes.forEach(s => { - if (!data.has(s)) throw new Error(`checkOnce, ${s}?`); - }); -} - -// helper function to find underlying types -// eslint-disable-next-line no-unused-vars -function underlying(n: ts.Node | undefined, f: (n: ts.Node) => void) { - if (!n) return; - const ff = function (n: ts.Node) { - underlying(n, f); - }; - if (ts.isIdentifier(n)) { - f(n); - } else if ( - n.kind == ts.SyntaxKind.StringKeyword || - n.kind == ts.SyntaxKind.NumberKeyword || - n.kind == ts.SyntaxKind.AnyKeyword || - n.kind == ts.SyntaxKind.UnknownKeyword || - n.kind == ts.SyntaxKind.NullKeyword || - n.kind == ts.SyntaxKind.BooleanKeyword || - n.kind == ts.SyntaxKind.ObjectKeyword || - n.kind == ts.SyntaxKind.VoidKeyword) { - // nothing to do - } else if (ts.isTypeReferenceNode(n)) { - f(n.typeName); - } else if (ts.isArrayTypeNode(n)) { - underlying(n.elementType, f); - } else if (ts.isHeritageClause(n)) { - n.types.forEach(ff); - } else if (ts.isExpressionWithTypeArguments(n)) { - underlying(n.expression, f); - } else if (ts.isPropertySignature(n)) { - underlying(n.type, f); - } else if (ts.isTypeLiteralNode(n)) { - n.members.forEach(ff); - } else if (ts.isUnionTypeNode(n) || ts.isIntersectionTypeNode(n)) { - n.types.forEach(ff); - } else if (ts.isIndexSignatureDeclaration(n)) { - underlying(n.type, f); - } else if (ts.isParenthesizedTypeNode(n)) { - underlying(n.type, f); - } else if ( - ts.isLiteralTypeNode(n) || ts.isVariableStatement(n) || - ts.isTupleTypeNode(n)) { - // we only see these in moreTypes, but they are handled elsewhere - } else if (ts.isEnumMember(n)) { - if (ts.isStringLiteral(n.initializer)) return; - throw new Error(`472 EnumMember ${strKind(n.initializer)} ${n.name.getText()}`); - } else { - throw new Error(`474 saw ${strKind(n)} in underlying. ${n.getText()} at ${loc(n)}`); - } -} - -// find all the types implied by seenTypes. -// Simplest way to the transitive closure is to stabilize the size of seenTypes -// but it is slow -function moreTypes() { - const extra = function (s: string) { - if (!data.has(s)) throw new Error(`moreTypes needs ${s}`); - seenTypes.set(s, data.get(s)); - }; - rpcTypes.forEach(extra); // all the types needed by the rpcs - // needed in enums.go (or elsewhere) - extra('InitializeError'); - extra('WatchKind'); - extra('FoldingRangeKind'); - // not sure why these weren't picked up - extra('DidChangeWatchedFilesRegistrationOptions'); - extra('WorkDoneProgressBegin'); - extra('WorkDoneProgressReport'); - extra('WorkDoneProgressEnd'); - let old = 0; - do { - old = seenTypes.size; - - const m = new Map<string, Data>(); - const add = function (n: ts.Node) { - const nm = goName(n.getText()); - if (seenTypes.has(nm) || m.has(nm)) return; - if (data.get(nm)) { - m.set(nm, data.get(nm)); - } - }; - // expect all the heritage clauses have single Identifiers - const h = function (n: ts.Node) { - underlying(n, add); - }; - const f = function (x: ts.NodeArray<ts.Node>) { - x.forEach(h); - }; - seenTypes.forEach((d: Data) => d && f(d.as)); - // find the types in the properties - seenTypes.forEach((d: Data) => d && f(d.properties)); - // and in the alias and in the statements and in the enums - seenTypes.forEach((d: Data) => d && underlying(d.alias, add)); - seenTypes.forEach((d: Data) => d && f(d.statements)); - seenTypes.forEach((d: Data) => d && f(d.enums)); - m.forEach((d, k) => seenTypes.set(k, d)); - } - while (seenTypes.size != old) - ; -} - -function cleanData() { // middle pass - // seenTypes contains all the top-level types. - seenTypes.forEach((d) => { - if (d.kind == 'alias') mergeAlias(d); - }); -} - -function sameType(a: ts.TypeNode, b: ts.TypeNode): boolean { - if (a.kind !== b.kind) return false; - if (a.kind === ts.SyntaxKind.BooleanKeyword) return true; - if (a.kind === ts.SyntaxKind.StringKeyword) return true; - if (ts.isTypeReferenceNode(a) && ts.isTypeReferenceNode(b) && - a.typeName.getText() === b.typeName.getText()) return true; - if (ts.isArrayTypeNode(a) && ts.isArrayTypeNode(b)) return sameType(a.elementType, b.elementType); - if (ts.isTypeLiteralNode(a) && ts.isTypeLiteralNode(b)) { - if (a.members.length !== b.members.length) return false; - if (a.members.length === 1) return a.members[0].name.getText() === b.members[0].name.getText(); - if (loc(a) === loc(b)) return true; - } - throw new Error(`544 sameType? ${strKind(a)} ${strKind(b)} ${a.getText()}`); -} -type CreateMutable<Type> = { - -readonly [Property in keyof Type]: Type[Property]; -}; -type propMap = Map<string, ts.PropertySignature>; -function propMapSet(pm: propMap, name: string, v: ts.PropertySignature) { - if (!pm.get(name)) { - try { getComments(v); } catch (e) { console.log(`552 ${name} ${e}`); } - pm.set(name, v); - return; - } - const a = pm.get(name).type; - const b = v.type; - if (sameType(a, b)) { - return; - } - if (ts.isTypeReferenceNode(a) && ts.isTypeLiteralNode(b)) { - const x = mergeTypeRefLit(a, b); - const fake: CreateMutable<ts.PropertySignature> = v; - fake['type'] = x; - check(fake as ts.PropertySignature, '565'); - pm.set(name, fake as ts.PropertySignature); - return; - } - if (ts.isTypeLiteralNode(a) && ts.isTypeLiteralNode(b)) { - const x = mergeTypeLitLit(a, b); - const fake: CreateMutable<ts.PropertySignature> = v; - fake['type'] = x; - check(fake as ts.PropertySignature, '578'); - pm.set(name, fake as ts.PropertySignature); - return; - } - console.log(`577 ${pm.get(name).getText()}\n${v.getText()}`); - throw new Error(`578 should merge ${strKind(a)} and ${strKind(b)} for ${name}`); -} -function addToProperties(pm: propMap, tn: ts.TypeNode | undefined, prefix = '') { - if (!tn) return; - if (ts.isTypeReferenceNode(tn)) { - const d = seenTypes.get(goName(tn.typeName.getText())); - if (tn.typeName.getText() === 'T') return; - if (!d) throw new Error(`584 ${tn.typeName.getText()} not found`); - if (d.properties.length === 0 && d.alias === undefined) return; - if (d.alias !== undefined) { - if (ts.isIntersectionTypeNode(d.alias)) { - d.alias.types.forEach((tn) => addToProperties(pm, tn, prefix)); // prefix? - return; - } - } - d.properties.forEach((ps) => { - const name = `${prefix}.${ps.name.getText()}`; - propMapSet(pm, name, ps); - addToProperties(pm, ps.type, name); - }); - } else if (strKind(tn) === 'TypeLiteral') { - if (!ts.isTypeLiteralNode(tn)) new Error(`599 ${strKind(tn)}`); - tn.forEachChild((child: ts.Node) => { - if (ts.isPropertySignature(child)) { - const name = `${prefix}.${child.name.getText()}`; - propMapSet(pm, name, child); - addToProperties(pm, child.type, name); - } else if (!ts.isIndexSignatureDeclaration(child)) { - // ignoring IndexSignatures, seen as relatedDocument in - // RelatedFullDocumentDiagnosticReport - throw new Error(`608 ${strKind(child)} ${loc(child)}`); - } - }); - } -} -function deepProperties(d: Data): propMap | undefined { - let properties: propMap = new Map<string, ts.PropertySignature>(); - if (!d.alias || !ts.isIntersectionTypeNode(d.alias)) return undefined; - d.alias.types.forEach((ts) => addToProperties(properties, ts)); - return properties; -} - -function mergeAlias(d: Data) { - const props = deepProperties(d); - if (!props) return; // nothing merged - // now each element of props should have length 1 - // change d to merged, toss its alias field, fill in its properties - const v: ts.PropertySignature[] = []; - props.forEach((ps, nm) => { - const xlen = nm.split('.').length; - if (xlen !== 2) return; // not top-level - v.push(ps); - }); - d.kind = 'interface'; - d.alias = undefined; - d.properties = ts.factory.createNodeArray(v); -} - -function mergeTypeLitLit(a: ts.TypeLiteralNode, b: ts.TypeLiteralNode): ts.TypeLiteralNode { - const v = new Map<string, ts.TypeElement>(); // avoid duplicates - a.members.forEach((te) => v.set(te.name.getText(), te)); - b.members.forEach((te) => v.set(te.name.getText(), te)); - const x: ts.TypeElement[] = []; - v.forEach((te) => x.push(te)); - const fake: CreateMutable<ts.TypeLiteralNode> = a; - fake['members'] = ts.factory.createNodeArray(x); - check(fake as ts.TypeLiteralNode, '643'); - return fake as ts.TypeLiteralNode; -} - -function mergeTypeRefLit(a: ts.TypeReferenceNode, b: ts.TypeLiteralNode): ts.TypeLiteralNode { - const d = seenTypes.get(goName(a.typeName.getText())); - if (!d) throw new Error(`644 name ${a.typeName.getText()} not found`); - const typ = d.me; - if (!ts.isInterfaceDeclaration(typ)) throw new Error(`646 got ${strKind(typ)} not InterfaceDecl`); - const v = new Map<string, ts.TypeElement>(); // avoid duplicates - typ.members.forEach((te) => v.set(te.name.getText(), te)); - b.members.forEach((te) => v.set(te.name.getText(), te)); - const x: ts.TypeElement[] = []; - v.forEach((te) => x.push(te)); - - const w = ts.factory.createNodeArray(x); - const fk: CreateMutable<ts.TypeLiteralNode> = b; - fk['members'] = w; - (fk['members'] as { pos: number })['pos'] = b.members.pos; - (fk['members'] as { end: number })['end'] = b.members.end; - check(fk as ts.TypeLiteralNode, '662'); - return fk as ts.TypeLiteralNode; -} - -// check that constructed nodes still have associated text -function check(n: ts.Node, loc: string) { - try { getComments(n); } catch (e) { console.log(`check at ${loc} ${e}`); } - try { n.getText(); } catch (e) { console.log(`text check at ${loc}`); } -} - -let typesOut = new Array<string>(); -let constsOut = new Array<string>(); - -// generate Go types -function toGo(d: Data, nm: string) { - if (!d) return; // this is probably a generic T - if (d.name.startsWith('Inner') || d.name === 'WindowClientCapabilities') return; // removed by alias processing - if (d.name === 'Integer' || d.name === 'Uinteger') return; // unneeded - switch (d.kind) { - case 'alias': - goTypeAlias(d, nm); break; - case 'module': goModule(d, nm); break; - case 'enum': goEnum(d, nm); break; - case 'interface': goInterface(d, nm); break; - default: - throw new Error( - `672: more cases in toGo ${nm} ${d.kind}`); - } -} - -// these fields need a * and are not covered by the code -// that calls isStructType. -var starred: [string, string][] = [ - ['TextDocumentContentChangeEvent', 'range'], ['CodeAction', 'command'], - ['CodeAction', 'disabled'], - ['DidSaveTextDocumentParams', 'text'], ['CompletionItem', 'command'], - ['Diagnostic', 'codeDescription'] -]; - -// generate Go code for an interface -function goInterface(d: Data, nm: string) { - let ans = `type ${goName(nm)} struct {\n`; - - // generate the code for each member - const g = function (n: ts.PropertySignature) { - if (!ts.isPropertySignature(n)) - throw new Error(`expected PropertySignature got ${strKind(n)} `); - ans = ans.concat(getComments(n)); - const json = u.JSON(n); - let gt = goType(n.type, n.name.getText()); - if (gt == d.name) gt = '*' + gt; // avoid recursive types (SelectionRange) - // there are several cases where a * is needed - // (putting * in front of too many things breaks uses of CodeActionKind) - starred.forEach(([a, b]) => { - if (d.name == a && n.name.getText() == b) { - gt = '*' + gt; - } - }); - ans = ans.concat(`${goName(n.name.getText())} ${gt}`, json, '\n'); - }; - d.properties.forEach(g); - // heritage clauses become embedded types - // check they are all Identifiers - const f = function (n: ts.ExpressionWithTypeArguments) { - if (!ts.isIdentifier(n.expression)) - throw new Error(`Interface ${nm} heritage ${strKind(n.expression)} `); - if (n.expression.getText() === 'Omit') return; // Type modification type - ans = ans.concat(goName(n.expression.getText()), '\n'); - }; - d.as.forEach((n: ts.HeritageClause) => n.types.forEach(f)); - ans = ans.concat('}\n'); - typesOut.push(getComments(d.me)); - typesOut.push(ans); -} - -// generate Go code for a module (const declarations) -// Generates type definitions, and named constants -function goModule(d: Data, nm: string) { - if (d.generics.length > 0 || d.as.length > 0) { - throw new Error(`743 goModule: unexpected for ${nm} - `); - } - // all the statements should be export const <id>: value - // or value = value - // They are VariableStatements with x.declarationList having a single - // VariableDeclaration - let isNumeric = false; - const f = function (n: ts.Statement, i: number) { - if (!ts.isVariableStatement(n)) { - throw new Error(`753 ${nm} ${i} expected VariableStatement, - got ${strKind(n)}`); - } - const c = getComments(n); - const v = n.declarationList.declarations[0]; // only one - - if (!v.initializer) - throw new Error(`760 no initializer ${nm} ${i} ${v.name.getText()}`); - isNumeric = strKind(v.initializer) == 'NumericLiteral'; - if (c != '') constsOut.push(c); // no point if there are no comments - // There are duplicates. - const cname = constName(goName(v.name.getText()), nm); - let val = v.initializer.getText(); - val = val.split('\'').join('"'); // useless work for numbers - constsOut.push(`${cname} ${nm} = ${val}`); - }; - d.statements.forEach(f); - typesOut.push(getComments(d.me)); - // Or should they be type aliases? - typesOut.push(`type ${nm} ${isNumeric ? 'float64' : 'string'}`); -} - -// generate Go code for an enum. Both types and named constants -function goEnum(d: Data, nm: string) { - let isNumeric = false; - const f = function (v: ts.EnumMember, j: number) { // same as goModule - if (!v.initializer) - throw new Error(`goEnum no initializer ${nm} ${j} ${v.name.getText()}`); - isNumeric = strKind(v.initializer) == 'NumericLiteral'; - const c = getComments(v); - const cname = constName(goName(v.name.getText()), nm); - let val = v.initializer.getText(); - val = val.split('\'').join('"'); // replace quotes. useless work for numbers - constsOut.push(`${c}${cname} ${nm} = ${val}`); - }; - d.enums.forEach(f); - typesOut.push(getComments(d.me)); - // Or should they be type aliases? - typesOut.push(`type ${nm} ${isNumeric ? 'float64' : 'string'}`); -} - -// generate code for a type alias -function goTypeAlias(d: Data, nm: string) { - if (d.as.length != 0 || d.generics.length != 0) { - if (nm != 'ServerCapabilities') - throw new Error(`${nm} has extra fields(${d.as.length},${d.generics.length}) ${d.me.getText()}`); - } - typesOut.push(getComments(d.me)); - // d.alias doesn't seem to have comments - let aliasStr = goName(nm) == 'DocumentURI' ? ' ' : ' = '; - if (nm == 'PrepareSupportDefaultBehavior') { - // code-insiders is sending a bool, not a number. PJW: check this after Feb/2021 - // (and gopls never looks at it anyway) - typesOut.push(`type ${goName(nm)}${aliasStr}interface{}\n`); - return; - } - typesOut.push(`type ${goName(nm)}${aliasStr}${goType(d.alias, nm)}\n`); -} - -// return a go type and maybe an assocated javascript tag -function goType(n: ts.TypeNode | undefined, nm: string): string { - if (!n) throw new Error(`goType undefined for ${nm}`); - if (n.getText() == 'T') return 'interface{}'; // should check it's generic - if (ts.isTypeReferenceNode(n)) { - // DocumentDiagnosticReportKind.unChanged (or .new) value is "new" or "unChanged" - if (n.getText().startsWith('DocumentDiagnostic')) return 'string'; - switch (n.getText()) { - case 'integer': return 'int32'; - case 'uinteger': return 'uint32'; - default: return goName(n.typeName.getText()); // avoid <T> - } - } else if (ts.isUnionTypeNode(n)) { - return goUnionType(n, nm); - } else if (ts.isIntersectionTypeNode(n)) { - return goIntersectionType(n, nm); - } else if (strKind(n) == 'StringKeyword') { - return 'string'; - } else if (strKind(n) == 'NumberKeyword') { - return 'float64'; - } else if (strKind(n) == 'BooleanKeyword') { - return 'bool'; - } else if (strKind(n) == 'AnyKeyword' || strKind(n) == 'UnknownKeyword') { - return 'interface{}'; - } else if (strKind(n) == 'NullKeyword') { - return 'nil'; - } else if (strKind(n) == 'VoidKeyword' || strKind(n) == 'NeverKeyword') { - return 'void'; - } else if (strKind(n) == 'ObjectKeyword') { - return 'interface{}'; - } else if (ts.isArrayTypeNode(n)) { - if (nm === 'arguments') { - // Command and ExecuteCommandParams - return '[]json.RawMessage'; - } - return `[]${goType(n.elementType, nm)}`; - } else if (ts.isParenthesizedTypeNode(n)) { - return goType(n.type, nm); - } else if (ts.isLiteralTypeNode(n)) { - return strKind(n.literal) == 'StringLiteral' ? 'string' : 'float64'; - } else if (ts.isTypeLiteralNode(n)) { - // these are anonymous structs - const v = goTypeLiteral(n, nm); - return v; - } else if (ts.isTupleTypeNode(n)) { - if (n.getText() == '[number, number]') return '[]float64'; - throw new Error(`goType unexpected Tuple ${n.getText()}`); - } - throw new Error(`${strKind(n)} goType unexpected ${n.getText()} for ${nm}`); -} - -// The choice is uniform interface{}, or some heuristically assigned choice, -// or some better sytematic idea I haven't thought of. Using interface{} -// is, in practice, impossibly complex in the existing code. -function goUnionType(n: ts.UnionTypeNode, nm: string): string { - let help = `/*${n.getText()}*/`; // show the original as a comment - // There are some bad cases with newlines: - // range?: boolean | {\n }; - // full?: boolean | {\n /**\n * The server supports deltas for full documents.\n */\n delta?: boolean;\n } - // These are handled specially: - if (nm == 'range') help = help.replace(/\n/, ''); - if (nm == 'full' && help.indexOf('\n') != -1) { - help = '/*boolean | <elided struct>*/'; - } - // handle all the special cases - switch (n.types.length) { - case 2: { - const a = strKind(n.types[0]); - const b = strKind(n.types[1]); - if (a == 'NumberKeyword' && b == 'StringKeyword') { // ID - return `interface{} ${help}`; - } - // for null, b is not useful (LiternalType) - if (n.types[1].getText() === 'null') { - if (nm == 'textDocument/codeAction') { - // (Command | CodeAction)[] | null - return `[]CodeAction ${help}`; - } - let v = goType(n.types[0], 'a'); - return `${v} ${help}`; - } - if (a == 'BooleanKeyword') { // usually want bool - if (nm == 'codeActionProvider') return `interface{} ${help}`; - if (nm == 'renameProvider') return `interface{} ${help}`; - if (nm == 'full') return `interface{} ${help}`; // there's a struct - if (nm == 'save') return `${goType(n.types[1], '680')} ${help}`; - return `${goType(n.types[0], 'b')} ${help}`; - } - if (b == 'ArrayType') return `${goType(n.types[1], 'c')} ${help}`; - if (help.includes('InsertReplaceEdit') && n.types[0].getText() == 'TextEdit') { - return `*TextEdit ${help}`; - } - if (a == 'TypeReference') { - if (nm == 'edits') return `${goType(n.types[0], '901')} ${help}`; - if (a == b) return `interface{} ${help}`; - if (nm == 'code') return `interface{} ${help}`; - if (nm == 'editRange') return `${goType(n.types[0], '904')} ${help}`; - if (nm === 'location') return `${goType(n.types[0], '905')} ${help}`; - } - if (a == 'StringKeyword') return `string ${help}`; - if (a == 'TypeLiteral' && nm == 'TextDocumentContentChangeEvent') { - return `${goType(n.types[0], nm)}`; - } - if (a == 'TypeLiteral' && b === 'TypeLiteral') { - // DocumentDiagnosticReport - // the first one includes the second one - return `${goType(n.types[0], '9d')}`; - } - throw new Error(`911 ${nm}: a:${a}/${goType(n.types[0], '9a')} b:${b}/${goType(n.types[1], '9b')} ${loc(n)}`); - } - case 3: { - const aa = strKind(n.types[0]); - const bb = strKind(n.types[1]); - const cc = strKind(n.types[2]); - if (nm === 'workspace/symbol') return `${goType(n.types[0], '930')} ${help}`; - if (nm == 'DocumentFilter' || nm == 'NotebookDocumentFilter' || nm == 'TextDocumentFilter') { - // not really a union. the first is enough, up to a missing - // omitempty but avoid repetitious comments - return `${goType(n.types[0], 'g')}`; - } - if (nm == 'textDocument/documentSymbol') { - return `[]interface{} ${help}`; - } - if (aa == 'TypeReference' && bb == 'ArrayType' && (cc == 'NullKeyword' || cc === 'LiteralType')) { - return `${goType(n.types[0], 'd')} ${help}`; - } - if (aa == 'TypeReference' && bb == aa && cc == 'ArrayType') { - // should check that this is Hover.Contents - return `${goType(n.types[0], 'e')} ${help}`; - } - if (aa == 'ArrayType' && bb == 'TypeReference' && (cc == 'NullKeyword' || cc === 'LiteralType')) { - // check this is nm == 'textDocument/completion' - return `${goType(n.types[1], 'f')} ${help}`; - } - if (aa == 'LiteralType' && bb == aa && cc == aa) return `string ${help}`; - // keep this for diagnosing unexpected interface{} results - // console.log(`931, interface{} for ${aa}/${goType(n.types[0], 'g')},${bb}/${goType(n.types[1], 'h')},${cc}/${goType(n.types[2], 'i')} ${nm}`); - break; - } - case 4: - if (nm == 'documentChanges') return `TextDocumentEdit ${help} `; - if (nm == 'textDocument/prepareRename') { - // these names have to be made unique - const genName = `${goName("prepareRename")}${extraTypes.size}Gn`; - extraTypes.set(genName, [`Range Range \`json:"range"\` - Placeholder string \`json:"placeholder"\``]); - return `${genName} ${help} `; - } - break; - case 8: // LSPany - break; - default: - throw new Error(`957 goUnionType len=${n.types.length} nm=${nm} ${n.getText()}`); - } - - // Result will be interface{} with a comment - let isLiteral = true; - let literal = 'string'; - let res = 'interface{} /* '; - n.types.forEach((v: ts.TypeNode, i: number) => { - // might get an interface inside: - // (Command | CodeAction)[] | null - let m = goType(v, nm); - if (m.indexOf('interface') != -1) { - // avoid nested comments - m = m.split(' ')[0]; - } - m = m.split('\n').join('; '); // sloppy: struct{; - res = res.concat(`${i == 0 ? '' : ' | '}`, m); - if (!ts.isLiteralTypeNode(v)) isLiteral = false; - else literal = strKind(v.literal) == 'StringLiteral' ? 'string' : 'number'; - }); - if (!isLiteral) { - return res + '*/'; - } - // I don't think we get here - // trace?: 'off' | 'messages' | 'verbose' should get string - return `${literal} /* ${n.getText()} */`; -} - -// some of the intersection types A&B are ok as struct{A;B;} and some -// could be expanded, and ClientCapabilites has to be expanded, -// at least for workspace. It's possible to check algorithmically, -// but much simpler just to check explicitly. -function goIntersectionType(n: ts.IntersectionTypeNode, nm: string): string { - if (nm == 'ClientCapabilities') return expandIntersection(n); - //if (nm == 'ServerCapabilities') return expandIntersection(n); // save for later consideration - let inner = ''; - n.types.forEach( - (t: ts.TypeNode) => { inner = inner.concat(goType(t, nm), '\n'); }); - return `struct{ \n${inner}} `; -} - -// for each of the intersected types, extract its components (each will -// have a Data with properties) extract the properties, and keep track -// of them by name. The names that occur once can be output. The names -// that occur more than once need to be combined. -function expandIntersection(n: ts.IntersectionTypeNode): string { - const bad = function (n: ts.Node, s: string) { - return new Error(`expandIntersection ${strKind(n)} ${s}`); - }; - let props = new Map<string, ts.PropertySignature[]>(); - for (const tp of n.types) { - if (!ts.isTypeReferenceNode(tp)) throw bad(tp, 'A'); - const d = data.get(goName(tp.typeName.getText())); - for (const p of d.properties) { - if (!ts.isPropertySignature(p)) throw bad(p, 'B'); - let v = props.get(p.name.getText()) || []; - v.push(p); - props.set(p.name.getText(), v); - } - } - let ans = 'struct {\n'; - for (const [k, v] of Array.from(props)) { - if (v.length == 1) { - const a = v[0]; - ans = ans.concat(getComments(a)); - ans = ans.concat(`${goName(k)} ${goType(a.type, k)} ${u.JSON(a)}\n`); - continue; - } - ans = ans.concat(`${goName(k)} struct {\n`); - for (let i = 0; i < v.length; i++) { - const a = v[i]; - if (ts.isTypeReferenceNode(a.type)) { - ans = ans.concat(getComments(a)); - ans = ans.concat(goName(a.type.typeName.getText()), '\n'); - } else if (ts.isTypeLiteralNode(a.type)) { - if (a.type.members.length != 1) throw bad(a.type, 'C'); - const b = a.type.members[0]; - if (!ts.isPropertySignature(b)) throw bad(b, 'D'); - ans = ans.concat(getComments(b)); - ans = ans.concat( - goName(b.name.getText()), ' ', goType(b.type, 'a'), u.JSON(b), '\n'); - } else { - throw bad(a.type, `E ${a.getText()} in ${goName(k)} at ${loc(a)}`); - } - } - ans = ans.concat('}\n'); - } - ans = ans.concat('}\n'); - return ans; -} - -// Does it make sense to use a pointer? -function isStructType(te: ts.TypeNode): boolean { - switch (strKind(te)) { - case 'UnionType': // really need to know which type will be chosen - case 'BooleanKeyword': - case 'StringKeyword': - case 'ArrayType': - return false; - case 'TypeLiteral': return false; // true makes for difficult compound constants - // but think more carefully to understands why starred is needed. - case 'TypeReference': { - if (!ts.isTypeReferenceNode(te)) throw new Error(`1047 impossible ${strKind(te)}`); - const d = seenTypes.get(goName(te.typeName.getText())); - if (d === undefined || d.properties.length == 0) return false; - if (d.properties.length > 1) return true; - // alias or interface with a single property (The alias is Uinteger, which we ignore later) - if (d.alias) return false; - const x = d.properties[0].type; - return isStructType(x); - } - default: throw new Error(`1055 indirectable> ${strKind(te)}`); - } -} - -function goTypeLiteral(n: ts.TypeLiteralNode, nm: string): string { - let ans: string[] = []; // in case we generate a new extra type - let res = 'struct{\n'; // the actual answer usually - const g = function (nx: ts.TypeElement) { - // add the json, as in goInterface(). Strange inside union types. - if (ts.isPropertySignature(nx)) { - let json = u.JSON(nx); - let typ = goType(nx.type, nx.name.getText()); - // }/*\n*/`json:v` is not legal, the comment is a newline - if (typ.includes('\n') && typ.indexOf('*/') === typ.length - 2) { - typ = typ.replace(/\n\t*/g, ' '); - } - const v = getComments(nx) || ''; - starred.forEach(([a, b]) => { - if (a != nm || b != typ.toLowerCase()) return; - typ = '*' + typ; - json = json.substring(0, json.length - 2) + ',omitempty"`'; - }); - if (typ[0] !== '*' && isStructType(nx.type)) typ = '*' + typ; - res = res.concat(`${v} ${goName(nx.name.getText())} ${typ}`, json, '\n'); - ans.push(`${v}${goName(nx.name.getText())} ${typ} ${json}\n`); - } else if (ts.isIndexSignatureDeclaration(nx)) { - const comment = nx.getText().replace(/[/]/g, ''); - if (nx.getText() == '[uri: string]: TextEdit[];') { - res = 'map[string][]TextEdit'; - } else if (nx.getText().startsWith('[id: ChangeAnnotationIdentifier]')) { - res = 'map[string]ChangeAnnotationIdentifier'; - } else if (nx.getText().startsWith('[uri: string')) { - res = 'map[string]interface{}'; - } else if (nx.getText().startsWith('[uri: DocumentUri')) { - res = 'map[DocumentURI][]TextEdit'; - } else if (nx.getText().startsWith('[key: string')) { - res = 'map[string]interface{}'; - } else { - throw new Error(`1100 handle ${nx.getText()} ${loc(nx)}`); - } - res += ` /*${comment}*/`; - ans.push(res); - return; - } else - throw new Error(`TypeLiteral had ${strKind(nx)}`); - }; - n.members.forEach(g); - // for some the generated type is wanted, for others it's not needed - if (!nm.startsWith('workspace')) { - if (res.startsWith('struct')) return res + '}'; // map[] is special - return res; - } - // these names have to be made unique - const genName = `${goName(nm)}${extraTypes.size}Gn`; - extraTypes.set(genName, ans); - return genName; -} - -// print all the types and constants and extra types -function outputTypes() { - // generate go types alphabeticaly - let v = Array.from(seenTypes.keys()); - v.sort(); - v.forEach((x) => toGo(seenTypes.get(x), x)); - u.prgo(u.computeHeader(true)); - u.prgo('import "encoding/json"\n\n'); - typesOut.forEach((s) => { - u.prgo(s); - // it's more convenient not to have to think about trailing newlines - // when generating types, but doc comments can't have an extra \n - if (s.indexOf('/**') < 0) u.prgo('\n'); - }); - u.prgo('\nconst (\n'); - constsOut.forEach((s) => { - u.prgo(s); - u.prgo('\n'); - }); - u.prgo(')\n'); - u.prgo('// Types created to name formal parameters and embedded structs\n'); - extraTypes.forEach((v, k) => { - u.prgo(` type ${k} struct {\n`); - v.forEach((s) => { - u.prgo(s); - u.prgo('\n'); - }); - u.prgo('}\n'); - }); -} - -// client and server ------------------ - -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', -}; - -// commonly used output -const notNil = `if len(r.Params()) > 0 { - return true, reply(ctx, nil, errors.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) -}`; - -// Go code for notifications. Side is client or server, m is the request -// method -function goNot(side: side, m: string) { - if (m == '$/cancelRequest') return; // handled specially in protocol.go - const n = not.get(m); - const a = goType(n.typeArguments[0], m); - const nm = methodName(m); - side.methods.push(sig(nm, a, '')); - const caseHdr = ` case "${m}": // notif`; - let case1 = notNil; - if (a != '' && a != 'void') { - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err:= ${side.name}.${nm}(ctx, ¶ms) - return true, reply(ctx, nil, err)`; - } else { - case1 = `err := ${side.name}.${nm}(ctx) - return true, reply(ctx, nil, err)`; - } - side.cases.push(`${caseHdr}\n${case1}`); - - const arg3 = a == '' || a == 'void' ? 'nil' : 'params'; - side.calls.push(` - func (s *${side.name}Dispatcher) ${sig(nm, a, '', true)} { - return s.sender.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(n.typeArguments[0], m); - let b = goType(n.typeArguments[1], m); - if (n.getText().includes('Type0')) { - b = a; - a = ''; // workspace/workspaceFolders and shutdown - } - u.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 != '') { - if (extraTypes.has('Param' + nm)) a = 'Param' + nm; - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - }`; - if (a === 'ParamInitialize') { - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - if _, ok := err.(*json.UnmarshalTypeError); !ok { - return true, sendParseError(ctx, reply, err) - } - }`; - } - } - const arg2 = a == '' ? '' : ', ¶ms'; - // if case2 is not explicitly typed string, typescript makes it a union of strings - let case2: string = `if err := ${side.name}.${nm}(ctx${arg2}); err != nil { - event.Error(ctx, "", err) - }`; - if (b != '' && b != 'void') { - case2 = `resp, err := ${side.name}.${nm}(ctx${arg2}) - return true, reply(ctx, resp, err)`; - } else { // response is nil - case2 = `err := ${side.name}.${nm}(ctx${arg2}) - return true, reply(ctx, nil, 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.sender.Call(ctx, "${m}", nil, nil)\n}`; - if (b != '' && b != 'void') { - const p2 = a == '' ? 'nil' : 'params'; - const returnType = indirect(b) ? `*${b}` : b; - callBody = `var result ${returnType} - if err := s.sender.Call(ctx, "${m}", ${p2}, &result); err != nil { - return nil, err - } - return result, nil - }`; - } else if (a != '') { - callBody = `return s.sender.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 { - let i = m.indexOf('/'); - let s = m.substring(i + 1); - let x = s[0].toUpperCase() + s.substring(1); - for (let j = x.indexOf('/'); j >= 0; j = x.indexOf('/')) { - let suffix = x.substring(j + 1); - suffix = suffix[0].toUpperCase() + suffix.substring(1); - let prefix = x.substring(0, j); - x = prefix + suffix; - } - if (seenNames.has(x)) { - // various Resolve and Diagnostic - x += m[0].toUpperCase() + m.substring(1, i); - } - seenNames.add(x); - return x; -} - -// used in sig and in goReq -function indirect(s: string): boolean { - if (s == '' || s == 'void') return false; - const skip = (x: string) => s.startsWith(x); - if (skip('[]') || skip('interface') || skip('Declaration') || - skip('Definition') || skip('DocumentSelector')) - return false; - return true; -} - -// Go signatures for methods. -function sig(nm: string, a: string, b: string, names?: boolean): string { - if (a.indexOf('struct') != -1) { - const v = a.split('\n'); - extraTypes.set(`Param${nm}`, v.slice(1, v.length - 1)); - a = 'Param' + nm; - } - if (a == 'void') - a = ''; - else if (a != '') { - if (names) - a = ', params *' + a; - else - a = ', *' + a; - } - let ret = 'error'; - if (b != '' && b != 'void') { - // avoid * when it is senseless - if (indirect(b)) b = '*' + b; - ret = `(${b}, error)`; - } - let start = `${nm}(`; - if (names) { - start = start + 'ctx '; - } - return `${start}context.Context${a}) ${ret}`; -} - -// write the request/notification code -function output(side: side) { - // make sure the output file exists - if (!side.outputFile) { - 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(u.computeHeader(false)); - f(` - import ( - "context" - "encoding/json" - - "golang.org/x/tools/internal/jsonrpc2" - errors "golang.org/x/xerrors" - ) - `); - 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}Dispatch(ctx context.Context, ${side.name} ${a}, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { - switch r.Method() {`); - side.cases.forEach((v) => { f(v); }); - f(` - default: - return false, nil - } - }`); - side.calls.forEach((v) => { f(v); }); -} - -// Handling of non-standard requests, so we can add gopls-specific calls. -function nonstandardRequests() { - server.methods.push( - 'NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error)'); - server.calls.push( - `func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { - var result interface{} - if err := s.sender.Call(ctx, method, params, &result); err != nil { - return nil, err - } - return result, nil - } - `); -} - -// ----- remember it's a scripting language -function main() { - if (u.gitHash != u.git()) { - throw new Error( - `git hash mismatch, wanted\n${u.gitHash} but source is at\n${u.git()}`); - } - u.createOutputFiles(); - parse(); - u.printAST(program); - // find the Requests and Nofificatations - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - ts.forEachChild(sourceFile, findRPCs); - } - } - // separate RPCs into client and server - setReceives(); - // visit every sourceFile collecting top-level type definitions - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - ts.forEachChild(sourceFile, genTypes); - } - } - // check that each thing occurs exactly once, and put pointers into - // seenTypes - checkOnce(); - // for each of Client and Server there are 3 parts to the output: - // 1. type X interface {methods} - // 2. func (h *serverHandler) Deliver(...) { switch r.method } - // 3. func (x *xDispatcher) Method(ctx, parm) - not.forEach( // notifications - (v, k) => { - receives.get(k) == 'client' ? goNot(client, k) : goNot(server, k); - }); - req.forEach( // requests - (v, k) => { - receives.get(k) == 'client' ? goReq(client, k) : goReq(server, k); - }); - nonstandardRequests(); - // find all the types implied by seenTypes and rpcs to try to avoid - // generating types that aren't used - moreTypes(); - // do merging - cleanData(); - // and print the Go code - outputTypes(); - console.log(`seen ${seenTypes.size + extraTypes.size}`); - output(client); - output(server); -} - -main(); |