打字稿:在编译时收获 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
});
我希望使用 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
});