devoups devoups - 2 months ago 21
TypeScript Question

TypeScript source parsing: Get class decorator names

I'm using the TypeScript compiler API to parse some source code and get class references.

The

Node
object corresponding to the class definition has a
decorators
array but I can't find a way to get the name of each decorator.

I used this example from the TypeScript wiki

///<reference path="typings/node/node.d.ts" />

import * as 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 documention 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: DocEntry[] = [];

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

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

return;

/** visit nodes finding exported classes */
function visit(node: ts.Node) {
// Only consider exported nodes
if (!isNodeExported(node)) {
return;
}

if (node.kind === ts.SyntaxKind.ClassDeclaration) {
// This is a top level class, get its symbol
let symbol = checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);
output.push(serializeClass(symbol));
// No need to walk any further, class expressions/inner declarations
// cannot be exported
}
else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
// This is a namespace, visit its children
ts.forEachChild(node, visit);
}
}

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

/** Serialize a class symbol infomration */
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())
};
}

/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (node.flags & ts.NodeFlags.Export) !== 0 || (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
}
}

generateDocumentation(process.argv.slice(2), {
target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
});

Answer

You can get first token in decorator expression and this will be function declaration. From function you can get any info like name, parameters, docstring.

I have extended example:

///<reference path="typings/node/node.d.ts" />

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

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

/** Generate documention 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: DocEntry[] = [];

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

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

    return;

    /** visit nodes finding exported classes */
    function visit(node: ts.Node) {
        // Only consider exported nodes
        if (!isNodeExported(node)) {
            return;
        }

        if (node.kind === ts.SyntaxKind.ClassDeclaration) {
            // This is a top level class, get its symbol



            output.push(serializeClass((<ts.ClassDeclaration>node)));
            // No need to walk any further, class expressions/inner declarations
            // cannot be exported
        }
        else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
            // This is a namespace, visit its children
            ts.forEachChild(node, visit);
        }
    }

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

    /** Serialize a class symbol infomration */
    function serializeClass(node: ts.ClassDeclaration) {
        let symbol = checker.getSymbolAtLocation(node.name);

        let details = serializeSymbol(symbol);
        // Get the construct signatures
        details.decorators = node.decorators.map(serializeDecorator);
        let constructorType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
        details.constructors = constructorType.getConstructSignatures().map(serializeSignature);

        return details;
    }

    function serializeDecorator(decorator: ts.Decorator) {
        let symbol = checker.getSymbolAtLocation(decorator.expression.getFirstToken());
        let decoratorType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
        let details = serializeSymbol(symbol);
        details.constructors = decoratorType.getCallSignatures().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())
        };
    }

    /** True if this is visible outside this file, false otherwise */
    function isNodeExported(node: ts.Node): boolean {
        return (node.flags & ts.NodeFlags.Export) !== 0 || (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
    }
}

generateDocumentation(process.argv.slice(2), {
    target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
});

And now for sample code like this:

function MyDecorator(myParam: string) {

}

@MyDecorator("myVal")
class MyTestClass {

}

I got output:

[
    {
        "name": "MyTestClass",
        "documentation": "",
        "type": "typeof MyTestClass",
        "decorators": [
            {
                "name": "MyDecorator",
                "documentation": "",
                "type": "(myParam: string) => void",
                "constructors": [
                    {
                        "parameters": [
                            {
                                "name": "myParam",
                                "documentation": "",
                                "type": "string"
                            }
                        ],
                        "returnType": "void",
                        "documentation": ""
                    }
                ]
            }
        ],
        "constructors": [
            {
                "parameters": [],
                "returnType": "MyTestClass",
                "documentation": ""
            }
        ]
    }
]
Comments