@ply-ct/ply
Version:
REST API Automated Testing
221 lines (204 loc) • 8.88 kB
text/typescript
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
import * as glob from 'glob';
export type Args = { [key: string]: string | number | boolean };
export interface Decorator {
file: string;
class: string;
method?: string;
decorator: string;
arg?: string;
args?: Args;
}
export class Ts {
readonly program: ts.Program;
readonly checker: ts.TypeChecker;
constructor(tsConfig = 'tsconfig.json', sourcePatterns = ['src/**/*.ts']) {
let files: string[] = [];
for (const sourcePattern of sourcePatterns) {
files = [...files, ...glob.sync(sourcePattern)];
}
const configPath = ts.findConfigFile('.', ts.sys.fileExists, tsConfig);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json' from " + path.resolve('.'));
}
const configContents = fs.readFileSync(configPath).toString();
const compilerOptions = ts.parseConfigFileTextToJson(configPath, configContents);
this.program = ts.createProgram(files, compilerOptions as ts.CompilerOptions);
this.checker = this.program.getTypeChecker();
}
scanClassDecorators(decs: string[]): Decorator[] {
let decorators: Decorator[] = [];
for (const sourceFile of this.program
.getSourceFiles()
.filter((sf) => !sf.isDeclarationFile)) {
ts.forEachChild(sourceFile, (node) => {
if (ts.isClassDeclaration(node) && node.name && Ts.isExported(node)) {
decorators = [
...decorators,
...this.findClassDecorators(sourceFile, node as ts.ClassDeclaration, decs)
];
}
});
}
return decorators;
}
findClassDecorators(
sourceFile: ts.SourceFile,
classDeclaration: ts.ClassDeclaration,
decs: string[]
): Decorator[] {
const classDecs: Decorator[] = [];
const classSymbol = this.checker.getSymbolAtLocation(<ts.Node>classDeclaration.name);
if (classSymbol) {
let decorators: ts.Decorator[] | undefined;
if ((ts as any).getDecorators) {
decorators = (ts as any).getDecorators(classDeclaration);
} else {
// old typescript compiler sdk incompatible
decorators = (classDeclaration as any).decorators;
}
if (decorators) {
for (const decorator of decorators) {
const decoratorSymbol = this.getDecoratorSymbol(decorator);
if (decoratorSymbol) {
const decoratorType = this.checker.getAliasedSymbol(decoratorSymbol).name;
if (decs.includes(decoratorType)) {
classDecs.push({
file: sourceFile.fileName,
class: classSymbol.name,
decorator: decoratorType,
...Ts.decoratorArgs(decorator)
});
}
}
}
}
}
return classDecs;
}
findMethodDecorators(classDecorator: Decorator, decs: string[]): Decorator[] {
const methodDecs: Decorator[] = [];
const classDeclaration = this.getClassDeclaration(
classDecorator.file,
classDecorator.class
);
if (classDeclaration) {
for (const methodDeclaration of Ts.methodDeclarations(classDeclaration)) {
const methodSymbol = this.checker.getSymbolAtLocation(
<ts.Node>methodDeclaration.name
);
let decorators: ts.Decorator[] | undefined;
if (methodSymbol) {
if ((ts as any).getDecorators) {
decorators = (ts as any).getDecorators(methodDeclaration);
} else {
// old typescript compiler sdk incompatible
decorators = (methodDeclaration as any).decorators;
}
if (decorators) {
for (const decorator of decorators) {
const decoratorSymbol = this.getDecoratorSymbol(decorator);
if (decoratorSymbol) {
const decoratorType =
this.checker.getAliasedSymbol(decoratorSymbol).name;
if (decs.includes(decoratorType)) {
methodDecs.push({
file: classDecorator.file,
class: classDecorator.class,
decorator: decoratorType,
method: methodSymbol.name,
...Ts.decoratorArgs(decorator)
});
}
}
}
}
}
}
}
return methodDecs;
}
getClassDeclaration(file: string, className: string): ts.ClassDeclaration | undefined {
const sourceFile = this.program.getSourceFile(file);
if (sourceFile) {
let classDeclaration: ts.ClassDeclaration | undefined;
ts.forEachChild(sourceFile, (node) => {
if (ts.isClassDeclaration(node) && node.name && Ts.isExported(node)) {
const classDecl = node as ts.ClassDeclaration;
const classSymbol = this.checker.getSymbolAtLocation(<ts.Node>classDecl.name);
if (classSymbol?.name === className) {
classDeclaration = classDecl;
}
}
});
return classDeclaration;
}
}
getDecoratorSymbol(decorator: ts.Decorator): ts.Symbol | undefined {
if (decorator.expression) {
const firstToken = decorator.expression.getFirstToken();
if (firstToken) {
return this.checker.getSymbolAtLocation(firstToken);
} else {
return this.checker.getSymbolAtLocation(decorator.expression);
}
}
}
static decoratorArgs(decorator: ts.Decorator): { arg?: string; args?: Args } {
const args: { arg?: string; args?: Args } = {};
if (decorator.expression.getChildCount() >= 3) {
let text = decorator.expression.getChildAt(2).getText().trim();
if (
decorator.expression.getChildAt(2).getChildCount() >= 3 &&
ts.isObjectLiteralExpression(decorator.expression.getChildAt(2).getChildAt(2))
) {
args.args = Ts.parseObjectLiteral(decorator.expression.getChildAt(2).getChildAt(2));
text = decorator.expression.getChildAt(2).getChildAt(0).getText().trim();
}
args.arg = text.substring(1, text.length - 1);
}
return args;
}
static methodDeclarations(classDeclaration: ts.ClassDeclaration): ts.MethodDeclaration[] {
const methodDeclaration: ts.MethodDeclaration[] = [];
classDeclaration.forEachChild((node) => {
if (ts.isMethodDeclaration(node) && !ts.isPrivateIdentifier(node)) {
methodDeclaration.push(node);
}
});
return methodDeclaration;
}
static symbolAtNode(node: ts.Node): ts.Symbol | undefined {
return (node as any).symbol;
}
static isExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
);
}
/**
* Only handles simple types
*/
static parseObjectLiteral(objLit: any): Args {
const res: Args = {};
for (const prop of objLit.properties) {
const propName = prop.name?.getText();
if (propName && (prop as any).initializer) {
if ((prop as any).initializer.text) {
res[propName] = (prop as any).initializer.text;
} else {
const textVal = (prop as any).initializer.getText();
if (textVal === 'true' || textVal === 'false') {
res[propName] = textVal === 'true';
} else if (parseInt(textVal)) {
res[propName] = parseInt(textVal);
}
}
}
}
return res;
}
}