打字稿:在编译时收获 class、属性 和方法细节

Typescript: harvest class, property and method details at Compile Time

我希望使用 Typescript 编译器获取 class、属性和方法信息。

我正在使用 nodejs 并希望根据我的服务器端 class 定义使用这些信息来构建客户端表单等。

我使用堆栈溢出作为开始取得了很好的进展,例如: Correct way of getting type for a variable declaration in a typescript AST? 但希望进一步扩展以获取 classes.json 文件中当前缺少的方法参数信息,如下所示。任何建议,将不胜感激。我的代码:

import ts from 'typescript';
import * as fs from "fs";

interface DocEntry {
    name?: string;
    fileName?: string;
    documentation?: string;
    type?: string;
    constructors?: DocEntry[];
    parameters?: DocEntry[];
    returnType?: string;
}

/** Generate documentation for all classes in a set of .ts files */
function generateDocumentation(
    fileNames: string[],
    options: ts.CompilerOptions
): void {
    // Build a program using the set of root file names in fileNames
    let program = ts.createProgram(fileNames, options);

    // Get the checker, we will use it to find more about classes
    let checker = program.getTypeChecker();
    let output = {
        component: [],
        fields: [],
        methods: []
    };

    // Visit every sourceFile in the program
    for (const sourceFile of program.getSourceFiles()) {
        if (!sourceFile.isDeclarationFile) {
            // Walk the tree to search for classes
            ts.forEachChild(sourceFile, visit);
        }
    }

    // print out the definitions
    fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4));

    return;

    /** visit nodes */
    function visit(node: ts.Node) {

        if (ts.isClassDeclaration(node) && node.name) {
            // This is a top level class, get its symbol
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                const details = serializeClass(symbol);
                output.component.push(details);
            }
            ts.forEachChild(node, visit);

        }
        else if (ts.isPropertyDeclaration(node)) {
            const x = 0;
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                output.fields.push(serializeClass(symbol));
            }
        } else if (ts.isMethodDeclaration(node)) {
            const x = 0;
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                output.methods.push(serializeClass(symbol));
            }
        }
    }

    /** Serialize a symbol into a json object */
    function serializeSymbol(symbol: ts.Symbol): DocEntry {
        return {
            name: symbol.getName(),
            documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
            type: checker.typeToString(
                checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
            )
        };
    }

    /** Serialize a class symbol information */
    function serializeClass(symbol: ts.Symbol) {
        let details = serializeSymbol(symbol);

        // Get the construct signatures
        let constructorType = checker.getTypeOfSymbolAtLocation(
            symbol,
            symbol.valueDeclaration!
        );
        details.constructors = constructorType
            .getConstructSignatures()
            .map(serializeSignature);
        return details;
    }

    /** Serialize a signature (call or construct) */
    function serializeSignature(signature: ts.Signature) {
        return {
            parameters: signature.parameters.map(serializeSymbol),
            returnType: checker.typeToString(signature.getReturnType()),
            documentation: ts.displayPartsToString(signature.getDocumentationComment(checker))
        };
    }
}

generateDocumentation(["source1.ts"], {
    target: ts.ScriptTarget.ES5,
    module: ts.ModuleKind.CommonJS
});

目标源文件source1.ts:

 * Documentation for C
 */
class C {
    /**bg2 is very cool*/
    bg2: number = 2;
    bg4: number = 4;
    bgA: string = "A";

    /**
     * constructor documentation
     * @param a my parameter documentation
     * @param b another parameter documentation
     */
    constructor(a: string, b: C) {
    }

    /** MethodA is an A type Method*/
    methodA(myarga1: string): number {
        return 22;
    }

    /** definitely a B grade Method
     * @param myargb1 is very argumentative*/
    methodB(myargb1: string): string {
        return "abc";
    }
}

生成 JSON 文件 classes.json:

{
    "component": [
        {
            "name": "C",
            "documentation": "Documentation for C",
            "type": "typeof C",
            "constructors": [
                {
                    "parameters": [
                        {
                            "name": "a",
                            "documentation": "my parameter documentation",
                            "type": "string"
                        },
                        {
                            "name": "b",
                            "documentation": "another parameter documentation",
                            "type": "C"
                        }
                    ],
                    "returnType": "C",
                    "documentation": "constructor documentation"
                }
            ]
        }
    ],
    "fields": [
        {
            "name": "bg2",
            "documentation": "bg2 is very cool",
            "type": "number",
            "constructors": []
        },
        {
            "name": "bg4",
            "documentation": "",
            "type": "number",
            "constructors": []
        },
        {
            "name": "bgA",
            "documentation": "",
            "type": "string",
            "constructors": []
        }
    ],
    "methods": [
        {
            "name": "methodA",
            "documentation": "MethodA is an A type Method",
            "type": "(myarga1: string) => number",
            "constructors": []
        },
        {
            "name": "methodB",
            "documentation": "definitely a B grade Method",
            "type": "(myargb1: string) => string",
            "constructors": []
        }
    ]
}

在访问函数中添加了对 ts.isMethodDeclaration(node) 的检查以获取方法详细信息。还添加了对多个文件和文档标签的支持(例如,@DummyTag 写在文档注释中,如:

/** @DummyTag Mary had a little lamb  */

所以新文件运行良好:

// @ts-ignore
import ts from 'typescript';
import * as fs from "fs";

interface DocEntry {
    name?: string;
    fileName?: string;
    documentation?: string;
    type?: string;
    constructors?: DocEntry[];
    parameters?: DocEntry[];
    returnType?: string;
    tags?: Record<string, string>;
}

/** Generate documentation for all classes in a set of .ts files */
function generateDocumentation(
    fileNames: string[],
    options: ts.CompilerOptions
): void {
    // Build a program using the set of root file names in fileNames
    let program = ts.createProgram(fileNames, options);
    console.log("ROOT FILES:",program.getRootFileNames());
    // Get the checker, we will use it to find more about classes
    let checker = program.getTypeChecker();

    let allOutput = [];
    let output = null;
let exportStatementFound = false;

    let currentMethod = null;
    let fileIndex = 0;
    // Visit the sourceFile for each "source file" in the program
    //ie don't use program.getSourceFiles() as it gets all the imports as well

    for (let i=0; i<fileNames.length; i++) {
        const fileName = fileNames[i];
        const sourceFile = program.getSourceFile(fileName);
        // console.log("sourceFile.kind:", sourceFile.kind);
        if (sourceFile.kind === ts.SyntaxKind.ImportDeclaration){
            console.log("IMPORT");
        }
        exportStatementFound = false;
        if (!sourceFile.isDeclarationFile) {
            // Walk the tree to search for classes
            output = {
                fileName: fileName,
                component: [],
                fields: [],
                methods: []
            };

            ts.forEachChild(sourceFile, visit);
            if (output) {
                allOutput.push(output);
            }
            if (!exportStatementFound){
                console.log("WARNING: no export statement found in:", fileName);
            }
        }
    }
    // print out the definitions
    fs.writeFileSync("classes.json", JSON.stringify(allOutput, undefined, 4));

    return;

    /** visit nodes */
    function visit(node: ts.Node) {
        if (!output){
            return;
        }
        if (node.kind === ts.SyntaxKind.ImportDeclaration){
            console.log("IMPORT");
            //output = null;
            return;
        }
        if (node.kind === ts.SyntaxKind.DefaultKeyword){
            console.log("DEFAULT");
            return;
        }
        if (node.kind === ts.SyntaxKind.ExportKeyword){
            exportStatementFound = true;
            console.log("EXPORT");
            return;
        }

        if (ts.isClassDeclaration(node) && node.name) {
            // This is a top level class, get its symbol
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                //need localSymbol for the name, if there is one because otherwise exported as "default"
                symbol = (symbol.valueDeclaration?.localSymbol)?symbol.valueDeclaration?.localSymbol: symbol;
                const details = serializeClass(symbol);
                output.component.push(details);
            }
            ts.forEachChild(node, visit);
        }
        else if (ts.isPropertyDeclaration(node)) {
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                output.fields.push(serializeField(symbol));
            }
        } else if (ts.isMethodDeclaration(node)) {
            let symbol = checker.getSymbolAtLocation(node.name);
            if (symbol) {
                currentMethod = serializeMethod(symbol);
                output.methods.push(currentMethod);
            }
            ts.forEachChild(node, visit);
        }


    }

    /** Serialize a symbol into a json object */
    function serializeSymbol(symbol: ts.Symbol): DocEntry {
        const tags = symbol.getJsDocTags();
        let tagMap = null;
        if (tags?.length){
            console.log("TAGS:", tags);
            for (let i=0; i<tags.length; i++){
                const tag = tags[i];
                if (tag.name !== "param"){
                    tagMap = tagMap?tagMap:{};
                    tagMap[tag.name] = tag.text;
                }
            }
        }
        return {
            name: symbol.getName(),
            documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
            type: checker.typeToString(
                checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
            ),
            tags: tagMap
        };
    }

    /** Serialize a class symbol information */
    function serializeClass(symbol: ts.Symbol) {
        let details = serializeSymbol(symbol);

        // Get the construct signatures
        let constructorType = checker.getTypeOfSymbolAtLocation(
            symbol,
            symbol.valueDeclaration!
        );
        details.constructors = constructorType
            .getConstructSignatures()
            .map(serializeSignature);
        return details;
    }

    function serializeField(symbol: ts.Symbol) {
        return serializeSymbol(symbol);
    }

    function serializeMethod(symbol: ts.Symbol) {
        let details = serializeSymbol(symbol);

        // Get the construct signatures
        let methodType = checker.getTypeOfSymbolAtLocation(
            symbol,
            symbol.valueDeclaration!
        );
        let callingDetails = methodType.getCallSignatures()
            .map(serializeSignature)["0"];
        details = {...details, ...callingDetails};
        return details;
    }

    /** Serialize a signature (call or construct) */
    function serializeSignature(signature: ts.Signature) {
        return {
            parameters: signature.parameters.map(serializeSymbol),
            returnType: checker.typeToString(signature.getReturnType()),
            documentation: ts.displayPartsToString(signature.getDocumentationComment(checker))
        };
    }
}

generateDocumentation(["source1.ts", "source2.ts"], {
    target: ts.ScriptTarget.ES5,
    module: ts.ModuleKind.CommonJS
});