/* 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 */ 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; group: string; /** * Enum member declaration in the form 'Foo = "bar"'. */ enumMemberDecl: string | undefined; } interface GatherState { declTexts: Map; } 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(), }; 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(), 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 = new Set(); for (const g of perOpStates) { for (const k of g.nameSet.values()) { allNames.add(k); } } const commonNames: Set = 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(); 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();