Я хочу собрать информацию о классе, свойствах и методах с помощью компилятора Typescript.
Я использую nodejs и хочу использовать информацию для создания форм на стороне клиента и т. д. на основе определений классов на стороне моего сервера.
Я добился хорошего прогресса, используя переполнение стека в качестве начала, например: Правильный способ получения типа для объявления переменной в машинописном AST?, но хотел бы расширить, чтобы получить информацию о параметрах метода, которая в настоящее время отсутствует в файле class.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 class.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.info("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.info("sourceFile.kind:", sourceFile.kind);
if (sourceFile.kind === ts.SyntaxKind.ImportDeclaration){
console.info("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.info("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.info("IMPORT");
//output = null;
return;
}
if (node.kind === ts.SyntaxKind.DefaultKeyword){
console.info("DEFAULT");
return;
}
if (node.kind === ts.SyntaxKind.ExportKeyword){
exportStatementFound = true;
console.info("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.info("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
});