【问题标题】:Typescript: harvest class, property and method details at Compile Time打字稿:在编译时收获类、属性和方法的详细信息
【发布时间】:2021-03-28 17:06:23
【问题描述】:

我希望使用 Typescript 编译器收集类、属性和方法信息。

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

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

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": []
        }
    ]
}

【问题讨论】:

    标签: typescript class methods properties abstract-syntax-tree


    【解决方案1】:

    在访问函数中添加了对 ts.isMethodDeclaration(node) 的检查,以捕获方法详细信息。还添加了对多个文件和文档标签的支持(例如,像在文档 cmets 中编写的 @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
    });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-19
      • 2020-07-10
      • 2018-01-23
      相关资源
      最近更新 更多