taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

extract.ts (10493B)


      1 /*
      2  This file is part of GNU Taler
      3  (C) 2022 Taler Systems S.A.
      4 
      5  GNU Taler is free software; you can redistribute it and/or modify it under the
      6  terms of the GNU General Public License as published by the Free Software
      7  Foundation; either version 3, or (at your option) any later version.
      8 
      9  GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13  You should have received a copy of the GNU General Public License along with
     14  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 import * as ts from "typescript";
     18 import * as fs from "fs/promises";
     19 import * as path from "path";
     20 import * as prettier from "prettier";
     21 
     22 if (process.argv.length != 4) {
     23   console.log(
     24     `usage: ${process.argv[0]} ${process.argv[1]} WALLET_CORE_REPO OUTFILE`
     25   );
     26   process.exit(2);
     27 }
     28 
     29 const walletRootDir = process.argv[2];
     30 const outfile = process.argv[3];
     31 
     32 const walletCoreDir = path.join(walletRootDir, "packages/taler-wallet-core");
     33 const excludedNames = new Set([
     34   "TalerErrorCode",
     35   "WalletBackupContentV1",
     36   "Array",
     37 ]);
     38 
     39 const configFile = ts.findConfigFile(
     40   walletCoreDir,
     41   ts.sys.fileExists,
     42   "tsconfig.json"
     43 );
     44 if (!configFile) throw Error("tsconfig.json not found");
     45 const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
     46 
     47 const { options, fileNames, errors } = ts.parseJsonConfigFileContent(
     48   config,
     49   ts.sys,
     50   walletCoreDir
     51 );
     52 
     53 const program = ts.createProgram({
     54   options,
     55   rootNames: fileNames,
     56   configFileParsingDiagnostics: errors,
     57 });
     58 
     59 const checker = program.getTypeChecker();
     60 
     61 const walletApiTypesFiles = `${walletCoreDir}/src/wallet-api-types.ts`;
     62 console.log("api types file:", walletApiTypesFiles);
     63 
     64 const sourceFile = program.getSourceFile(walletApiTypesFiles);
     65 
     66 if (!sourceFile) {
     67   throw Error();
     68 }
     69 
     70 const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
     71 
     72 const fileSymbol = program.getTypeChecker().getSymbolAtLocation(sourceFile);
     73 
     74 const expo = fileSymbol?.exports;
     75 if (!expo) {
     76   throw Error();
     77 }
     78 
     79 interface PerOpGatherState {
     80   opName: string;
     81   nameSet: Set<string>;
     82   group: string;
     83   /**
     84    * Enum member declaration in the form 'Foo = "bar"'.
     85    */
     86   enumMemberDecl: string | undefined;
     87 }
     88 
     89 interface GatherState {
     90   declTexts: Map<string, string>;
     91 }
     92 
     93 function gatherDecls(
     94   node: ts.Node,
     95   gatherState: GatherState,
     96   perOpState: PerOpGatherState
     97 ): void {
     98   switch (node.kind) {
     99     case ts.SyntaxKind.EnumDeclaration:
    100       // Always handled via parent
    101       return;
    102     case ts.SyntaxKind.Identifier:
    103     case ts.SyntaxKind.TypeReference: {
    104       console.log(`start typeref-or-id ${node.getText()}`);
    105       const type = checker.getTypeAtLocation(node);
    106       if (type.flags === ts.TypeFlags.String) {
    107         console.log("string!");
    108         break;
    109       }
    110       const symbol = type.aliasSymbol || type.symbol;
    111       if (!symbol) {
    112         console.log(`no type symbol for ${node.getText()}`);
    113         break;
    114       }
    115       const name = symbol.name;
    116       console.log(`symbol name: ${type.symbol?.name}`);
    117       console.log(`alias symbol name: ${type.aliasSymbol?.name}`);
    118       if (perOpState.nameSet.has(name)) {
    119         console.log(`already found ${name}`);
    120         break;
    121       }
    122       perOpState.nameSet.add(name);
    123       if (excludedNames.has(name)) {
    124         console.log("excluded!");
    125         break;
    126       }
    127       const decls = symbol.getDeclarations();
    128       decls?.forEach((decl) => {
    129         const sourceFilename = decl.getSourceFile().fileName;
    130         if (path.basename(sourceFilename).startsWith("lib.")) {
    131           return;
    132         }
    133         switch (decl.kind) {
    134           case ts.SyntaxKind.EnumMember: {
    135             gatherDecls(decl.parent, gatherState, perOpState);
    136             console.log("enum member", decl.getText());
    137             break;
    138           }
    139           case ts.SyntaxKind.InterfaceDeclaration:
    140           case ts.SyntaxKind.EnumDeclaration:
    141           case ts.SyntaxKind.TypeAliasDeclaration: {
    142             const declText = printer.printNode(
    143               ts.EmitHint.Unspecified,
    144               decl,
    145               decl.getSourceFile()!
    146             );
    147             gatherState.declTexts.set(name, declText);
    148             console.log(declText);
    149             break;
    150           }
    151           case ts.SyntaxKind.TypeLiteral:
    152             if (!type.aliasSymbol) {
    153               // Just free-standing type literal, no need to emit!
    154               break;
    155             }
    156             console.log(`got TypeLiteral for ${name}`);
    157             const declText = printer.printNode(
    158               ts.EmitHint.Unspecified,
    159               decl,
    160               decl.getSourceFile()!
    161             );
    162             gatherState.declTexts.set(name, `type ${name} = ${declText};`);
    163             console.log(declText);
    164             break;
    165           default:
    166             console.log(`unknown decl kind ${ts.SyntaxKind[decl.kind]}`);
    167             break;
    168         }
    169         gatherDecls(decl, gatherState, perOpState);
    170         console.log(`end typeref-or-id ${node.getText()}`);
    171       });
    172       break;
    173     }
    174     default:
    175       break;
    176   }
    177   console.log(`start syntax children for ${node.getText()}`);
    178   node.forEachChild((child) => {
    179     console.log(`syntax child: ${ts.SyntaxKind[child.kind]}`);
    180     gatherDecls(child, gatherState, perOpState);
    181   });
    182   console.log(`end syntax children for ${node.getText()}`);
    183   //console.log(`// unknown node kind ${ts.SyntaxKind[node.kind]}`);
    184   return;
    185 }
    186 
    187 function getOpEnumDecl(decl: ts.Declaration): string | undefined {
    188   console.log("getting OpEnumDecl")
    189   let enumMemberDecl: undefined | string = undefined;
    190   function walk(node: ts.Node, level: number = 0) {
    191     node.forEachChild((x) => {
    192       console.log(`child kind [${level}]: ${ts.SyntaxKind[x.kind]}`);
    193       console.log(x.getText());
    194       switch (x.kind) {
    195         case ts.SyntaxKind.PropertySignature: {
    196           const sig = x as ts.PropertySignature;
    197           if (sig.name.getText() == "op") {
    198             const type = checker.getTypeFromTypeNode(sig.type!);
    199             enumMemberDecl = type.symbol.declarations![0]!.getText();
    200           }
    201           break;
    202         }
    203       }
    204       walk(x, level + 1);
    205     });
    206   }
    207   walk(decl);
    208   return enumMemberDecl;
    209 }
    210 
    211 const main = async () => {
    212   const f = await fs.open(outfile, "w");
    213   const gatherState: GatherState = {
    214     declTexts: new Map<string, string>(),
    215   };
    216   const perOpStates: PerOpGatherState[] = [];
    217 
    218   let currentGroup: string = "Unknown Group";
    219 
    220   expo.forEach((v, k) => {
    221     if (!v.name.endsWith("Op")) {
    222       return;
    223     }
    224     console.log(`gathering documentation for export ${v.name}`);
    225     const decls = v.getDeclarations();
    226     if (!decls) {
    227       return;
    228     }
    229     console.log(`has ${decls.length} declarations`);
    230     decls.forEach((decl) => {
    231       console.log(`export decl, kind ${ts.SyntaxKind[decl.kind]}`);
    232 
    233       const commentRanges = ts.getLeadingCommentRanges(
    234         sourceFile.getFullText(),
    235         decl.getFullStart()
    236       );
    237       commentRanges?.forEach((r) => {
    238         const text = sourceFile.getFullText().slice(r.pos, r.end);
    239         console.log("comment text:", text);
    240         const groupPrefix = "group:";
    241         const loc = text.indexOf(groupPrefix);
    242         if (loc >= 0) {
    243           const groupName = text.slice(loc + groupPrefix.length);
    244           console.log("got new group", groupName);
    245           currentGroup = groupName;
    246         }
    247       });
    248 
    249       const perOpState: PerOpGatherState = {
    250         opName: v.name,
    251         nameSet: new Set<string>(),
    252         group: currentGroup,
    253         enumMemberDecl: getOpEnumDecl(decl),
    254       };
    255       let declText = printer.printNode(
    256         ts.EmitHint.Unspecified,
    257         decl,
    258         decl.getSourceFile()!
    259       );
    260       if (perOpState.enumMemberDecl) {
    261         declText = declText + `\n// ${perOpState.enumMemberDecl}\n`;
    262       }
    263       console.log("replacing group in", declText);
    264       // Remove group comments
    265       declText = declText.replace(/\/\/ group: [^\n]*[\n]/m, "");
    266       perOpState.nameSet.add(v.name);
    267       gatherState.declTexts.set(v.name, declText);
    268       gatherDecls(decl, gatherState, perOpState);
    269       perOpStates.push(perOpState);
    270     });
    271   });
    272 
    273   const allNames: Set<string> = new Set();
    274 
    275   for (const g of perOpStates) {
    276     for (const k of g.nameSet.values()) {
    277       allNames.add(k);
    278     }
    279   }
    280 
    281   const commonNames: Set<string> = new Set();
    282 
    283   for (const name of allNames) {
    284     let count = 0;
    285     for (const g of perOpStates) {
    286       for (const k of g.nameSet.values()) {
    287         if (name === k) {
    288           count++;
    289         }
    290       }
    291     }
    292     if (count > 1) {
    293       console.log(`common name: ${name}`);
    294       commonNames.add(name);
    295     }
    296   }
    297 
    298   const groups = new Set<string>();
    299   for (const g of perOpStates) {
    300     groups.add(g.group);
    301   }
    302 
    303   await f.write(`# Wallet-Core API Documentation\n`);
    304 
    305   await f.write(
    306     `This file is auto-generated from the [taler-typescript-core](https://git.taler.net/taler-typescript-core.git/tree/packages/taler-wallet-core/src/wallet-api-types.ts) repository.\n`
    307   );
    308 
    309   await f.write(`## Overview\n`);
    310   for (const g of groups.values()) {
    311     await f.write(`### ${g}\n`);
    312     for (const op of perOpStates) {
    313       if (op.group !== g) {
    314         continue;
    315       }
    316       await f.write(`* [${op.opName}](#${op.opName.toLowerCase()})\n`);
    317     }
    318   }
    319 
    320   await f.write(`## Operation Reference\n`);
    321   for (const g of perOpStates) {
    322     // Not yet supported, switch to myst first!
    323     // await f.write(`(${g.opName.toLowerCase()})=\n`);
    324     await f.write(`### ${g.opName}\n`);
    325     for (const name of g.nameSet.values()) {
    326       if (commonNames.has(name)) {
    327         continue;
    328       }
    329       const text = gatherState.declTexts.get(name);
    330       if (!text) {
    331         continue;
    332       }
    333       await f.write("```typescript\n");
    334       const formatted = prettier.format(text, {
    335         semi: true,
    336         parser: "typescript",
    337       });
    338       await f.write(`${formatted}\n`);
    339       await f.write("```\n");
    340     }
    341     await f.write("\n");
    342   }
    343 
    344   await f.write(`## Common Declarations\n`);
    345   for (const name of commonNames.values()) {
    346     const text = gatherState.declTexts.get(name);
    347     if (!text) {
    348       continue;
    349     }
    350     await f.write("```typescript\n");
    351     const formatted = prettier.format(text, {
    352       semi: true,
    353       parser: "typescript",
    354     });
    355     await f.write(`${formatted}`);
    356     await f.write("```\n");
    357   }
    358 
    359   await f.close();
    360 };
    361 
    362 main();