diff options
Diffstat (limited to 'extract-tsdefs/extract.ts')
-rw-r--r-- | extract-tsdefs/extract.ts | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/extract-tsdefs/extract.ts b/extract-tsdefs/extract.ts new file mode 100644 index 00000000..5be82479 --- /dev/null +++ b/extract-tsdefs/extract.ts @@ -0,0 +1,341 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import * as ts from "typescript"; +import * as fs from "fs/promises"; +import * as path from "path"; +import * as prettier from "prettier"; + +if (process.argv.length != 4) { + console.log( + `usage: ${process.argv[0]} ${process.argv[1]} WALLET_CORE_REPO OUTFILE` + ); + process.exit(2); +} + +const walletRootDir = process.argv[2]; +const outfile = process.argv[3]; + +const walletCoreDir = path.join(walletRootDir, "packages/taler-wallet-core"); +const excludedNames = new Set([ + "TalerErrorCode", + "WalletBackupContentV1", + "Array", +]); + +const configFile = ts.findConfigFile( + walletCoreDir, + ts.sys.fileExists, + "tsconfig.json" +); +if (!configFile) throw Error("tsconfig.json not found"); +const { config } = ts.readConfigFile(configFile, ts.sys.readFile); + +const { options, fileNames, errors } = ts.parseJsonConfigFileContent( + config, + ts.sys, + walletCoreDir +); + +const program = ts.createProgram({ + options, + rootNames: fileNames, + configFileParsingDiagnostics: errors, +}); + +const checker = program.getTypeChecker(); + +const walletApiTypesFiles = `${walletCoreDir}/src/wallet-api-types.ts`; +console.log("api types file:", walletApiTypesFiles); + +const sourceFile = program.getSourceFile(walletApiTypesFiles); + +if (!sourceFile) { + throw Error(); +} + +const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + +const fileSymbol = program.getTypeChecker().getSymbolAtLocation(sourceFile); + +const expo = fileSymbol?.exports; +if (!expo) { + throw Error(); +} + +interface PerOpGatherState { + opName: string; + nameSet: Set<string>; + group: string; + /** + * Enum member declaration in the form 'Foo = "bar"'. + */ + enumMemberDecl: string | undefined; +} + +interface GatherState { + declTexts: Map<string, string>; +} + +function gatherDecls( + node: ts.Node, + gatherState: GatherState, + perOpState: PerOpGatherState +): void { + switch (node.kind) { + case ts.SyntaxKind.EnumDeclaration: + // Always handled via parent + return; + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.TypeReference: { + console.log(`start typeref-or-id ${node.getText()}`); + const type = checker.getTypeAtLocation(node); + if (type.flags === ts.TypeFlags.String) { + console.log("string!"); + break; + } + const symbol = type.symbol || type.aliasSymbol; + if (!symbol) { + console.log(`no type symbol for ${node.getText()}`); + break; + } + const name = symbol.name; + console.log(`symbol name: ${type.symbol?.name}`); + console.log(`alias symbol name: ${type.aliasSymbol?.name}`); + if (perOpState.nameSet.has(name)) { + console.log("already found!"); + break; + } + perOpState.nameSet.add(name); + if (excludedNames.has(name)) { + console.log("excluded!"); + break; + } + const decls = symbol.getDeclarations(); + decls?.forEach((decl) => { + const sourceFilename = decl.getSourceFile().fileName; + if (path.basename(sourceFilename).startsWith("lib.")) { + return; + } + switch (decl.kind) { + case ts.SyntaxKind.EnumMember: { + gatherDecls(decl.parent, gatherState, perOpState); + console.log("enum member", decl.getText()); + break; + } + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: { + const declText = printer.printNode( + ts.EmitHint.Unspecified, + decl, + decl.getSourceFile()! + ); + gatherState.declTexts.set(name, declText); + console.log(declText); + break; + } + default: + console.log(`unknown decl kind ${ts.SyntaxKind[decl.kind]}`); + break; + } + gatherDecls(decl, gatherState, perOpState); + console.log(`end typeref-or-id ${node.getText()}`); + }); + break; + } + default: + break; + } + console.log(`syntax children for ${node.getText()}`); + node.forEachChild((child) => { + console.log(`syntax child: ${ts.SyntaxKind[child.kind]}`); + gatherDecls(child, gatherState, perOpState); + }); + //console.log(`// unknown node kind ${ts.SyntaxKind[node.kind]}`); + return; +} + +function getOpEnumDecl(decl: ts.Declaration): string | undefined { + let enumMemberDecl: undefined | string = undefined; + function walk(node: ts.Node) { + node.forEachChild((x) => { + console.log(`child kind: ${ts.SyntaxKind[x.kind]}`); + console.log(x.getText()); + switch (x.kind) { + case ts.SyntaxKind.PropertySignature: { + const sig = x as ts.PropertySignature; + if (sig.name.getText() == "op") { + const type = checker.getTypeFromTypeNode(sig.type!); + enumMemberDecl = type.symbol.declarations![0]!.getText(); + } + break; + } + } + walk(x); + }); + } + walk(decl); + return enumMemberDecl; +} + +const main = async () => { + const f = await fs.open(outfile, "w"); + const gatherState: GatherState = { + declTexts: new Map<string, string>(), + }; + const perOpStates: PerOpGatherState[] = []; + + let currentGroup: string = "Unknown Group"; + + expo.forEach((v, k) => { + if (!v.name.endsWith("Op")) { + return; + } + const decls = v.getDeclarations(); + decls?.forEach((decl) => { + console.log(`export decl, kind ${ts.SyntaxKind[decl.kind]}`); + + const commentRanges = ts.getLeadingCommentRanges( + sourceFile.getFullText(), + decl.getFullStart() + ); + commentRanges?.forEach((r) => { + const text = sourceFile.getFullText().slice(r.pos, r.end); + console.log("comment text:", text); + const groupPrefix = "group:"; + const loc = text.indexOf(groupPrefix); + if (loc >= 0) { + const groupName = text.slice(loc + groupPrefix.length); + console.log("got new group", groupName); + currentGroup = groupName; + } + }); + + const perOpState: PerOpGatherState = { + opName: v.name, + nameSet: new Set<string>(), + group: currentGroup, + enumMemberDecl: getOpEnumDecl(decl), + }; + let declText = printer.printNode( + ts.EmitHint.Unspecified, + decl, + decl.getSourceFile()! + ); + if (perOpState.enumMemberDecl) { + declText = declText + `\n// ${perOpState.enumMemberDecl}\n`; + } + console.log("replacing group in", declText); + // Remove group comments + declText = declText.replace(/\/\/ group: [^\n]*[\n]/m, ""); + perOpState.nameSet.add(v.name); + gatherState.declTexts.set(v.name, declText); + gatherDecls(decl, gatherState, perOpState); + perOpStates.push(perOpState); + }); + }); + + const allNames: Set<string> = new Set(); + + for (const g of perOpStates) { + for (const k of g.nameSet.values()) { + allNames.add(k); + } + } + + const commonNames: Set<string> = new Set(); + + for (const name of allNames) { + let count = 0; + for (const g of perOpStates) { + for (const k of g.nameSet.values()) { + if (name === k) { + count++; + } + } + } + if (count > 1) { + console.log(`common name: ${name}`); + commonNames.add(name); + } + } + + const groups = new Set<string>(); + for (const g of perOpStates) { + groups.add(g.group); + } + + await f.write(`# Wallet-Core API Documentation\n`); + + await f.write( + `This file is auto-generated from [wallet-core](https://git.taler.net/wallet-core.git/tree/packages/taler-wallet-core/src/wallet-api-types.ts).\n` + ); + + await f.write(`## Overview\n`); + for (const g of groups.values()) { + await f.write(`### ${g}\n`); + for (const op of perOpStates) { + if (op.group !== g) { + continue; + } + await f.write(`* [${op.opName}](#${op.opName.toLowerCase()})\n`); + } + } + + await f.write(`## Operation Reference\n`); + for (const g of perOpStates) { + // Not yet supported, switch to myst first! + // await f.write(`(${g.opName.toLowerCase()})=\n`); + await f.write(`### ${g.opName}\n`); + for (const name of g.nameSet.values()) { + if (commonNames.has(name)) { + continue; + } + const text = gatherState.declTexts.get(name); + if (!text) { + continue; + } + await f.write("```typescript\n"); + const formatted = prettier.format(text, { + semi: true, + parser: "typescript", + }); + await f.write(`${formatted}\n`); + await f.write("```\n"); + } + await f.write("\n"); + } + + await f.write(`## Common Declarations\n`); + for (const name of commonNames.values()) { + const text = gatherState.declTexts.get(name); + if (!text) { + continue; + } + await f.write("```typescript\n"); + const formatted = prettier.format(text, { + semi: true, + parser: "typescript", + }); + await f.write(`${formatted}`); + await f.write("```\n"); + } + + await f.close(); +}; + +main(); |