ph-dev-tools
Version:
Development Tools for PHibernate
309 lines (271 loc) • 9.75 kB
text/typescript
/**
* Created by Papa on 3/26/2016.
*/
import * as ts from "typescript";
import SyntaxKind = ts.SyntaxKind;
import {EntityCandidate} from "./EntityCandidate";
import {EntityCandidateRegistry} from "./EntityCandidateRegistry";
import {getParentClassName, getParentClassImport, getClassPath, isDecoratedAsEntity} from "./utils";
import {DocEntry, Decorator} from "./DocEntry";
import NodeArray = ts.NodeArray;
import PropertyAccessExpression = ts.PropertyAccessExpression;
import Identifier = ts.Identifier;
export var rootEntity = new EntityCandidate('Entity', null, null, 'PHibernate', true);
export var globalCandidateRegistry = new EntityCandidateRegistry(rootEntity);
/** Generate documention for all classes in a set of .ts files */
export function generateEntityDefinitions(
fileNames:string[],
options:ts.CompilerOptions
):EntityCandidate[] {
// 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();
var processedCandidateRegistry = new EntityCandidateRegistry(rootEntity);
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 globalCandidateRegistry.matchVerifiedEntities(processedCandidateRegistry);
/** 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);
let serializedClass = serializeClass(symbol, node.decorators);
if (serializedClass) {
output.push(serializedClass);
}
// 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 {
let docEntry = {
name: symbol.getName(),
decorators: [],
documentation: ts.displayPartsToString(symbol.getDocumentationComment()),
type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration))
};
let declarations = symbol.declarations;
if (declarations && declarations.length === 1) {
let declaration = symbol.declarations[0];
let decorators = declaration.decorators;
if (decorators) {
decorators.forEach((
tsDecorator:ts.Decorator
) => {
let decorator:Decorator = serializeDecorator(tsDecorator);
docEntry.decorators.push(decorator);
});
}
}
return docEntry;
}
function serializeDecorator(
tsDecorator:ts.Decorator
):Decorator {
let expression:ts.CallExpression = <ts.CallExpression>tsDecorator.expression;
let decoratorName:string;
if (expression.expression) {
decoratorName = (<ts.Identifier>expression.expression).text;
} else {
decoratorName = (<ts.Identifier><any>expression).text;
}
let values = [];
if (expression.arguments) {
expression.arguments.forEach((
argument:ts.Node
) => {
let value = parseObjectProperty(argument);
values.push(value);
})
}
let decorator:Decorator = {
name: decoratorName,
values: values
};
return decorator;
}
function parseObjectLiteralExpression(
objLitExpr:ts.ObjectLiteralExpression
):any {
let object = {};
if (objLitExpr.properties) {
objLitExpr.properties.forEach((
property:ts.PropertyAssignment
) => {
let propertyName = (<ts.Identifier>property.name).text;
let initializer = property.initializer;
let value = parseObjectProperty(initializer);
object[propertyName] = value;
});
}
return object;
}
function parseObjectProperty(
initializer:ts.Node
):any {
let value;
switch (initializer.kind) {
case ts.SyntaxKind.Identifier:
let identifier = <ts.Identifier>initializer;
switch (identifier.text) {
case 'undefined':
value = undefined;
break;
case 'NaN':
value = NaN;
break;
// Must be a variable declaration
default:
value = 'var ' + identifier.text;
break;
}
break;
case ts.SyntaxKind.NullKeyword:
value = null;
break;
case ts.SyntaxKind.RegularExpressionLiteral:
let regExp = <ts.Identifier>initializer;
value = convertRegExpStringToObject(regExp.text);
break;
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
value = (<ts.StringLiteral>initializer).text;
break;
case ts.SyntaxKind.TrueKeyword:
value = true;
break;
case ts.SyntaxKind.FalseKeyword:
value = false;
break;
case ts.SyntaxKind.NumericLiteral:
let numberText = (<any>initializer).text;
if (numberText.indexOf('.') > 0) {
value = parseFloat(numberText);
} else {
value = parseInt(numberText);
}
break;
case ts.SyntaxKind.NewExpression:
let newExpression = <ts.NewExpression>initializer;
let type = (<ts.Identifier>newExpression.expression).text;
value = 'new ' + type;
break;
case ts.SyntaxKind.ObjectLiteralExpression:
value = parseObjectLiteralExpression(<ts.ObjectLiteralExpression>initializer);
break;
case ts.SyntaxKind.ArrayLiteralExpression:
value = [];
let arrayLiteral = <ts.ArrayLiteralExpression>initializer;
arrayLiteral.elements.forEach((
element:ts.Node
) => {
let arrayValue = parseObjectProperty(element);
value.push(arrayValue);
});
break;
case ts.SyntaxKind.PropertyAccessExpression:
value = convertPropertyAccessExpressionToString(<PropertyAccessExpression>initializer);
break;
case ts.SyntaxKind.CallExpression:
throw `Function calls are not allowed as parameter values.`;
case ts.SyntaxKind.BinaryExpression:
throw `Expression are not allowed as parameter values.`;
default:
throw `Unsupported property initializer.kind: ${initializer.kind}`;
}
return value;
}
function convertPropertyAccessExpressionToString(
propAccessExrp:PropertyAccessExpression
):string {
let leftHandExrp = <Identifier>propAccessExrp.expression;
return `${leftHandExrp.text}.${propAccessExrp.name.text}`;
}
function convertRegExpStringToObject(
regExpString:string
):RegExp {
let firstIndexOfSlash = regExpString.indexOf('/');
if (firstIndexOfSlash < 0) {
return new RegExp(regExpString);
}
let lastIndexOfSlash = regExpString.lastIndexOf('/');
if (firstIndexOfSlash === lastIndexOfSlash) {
return new RegExp(regExpString);
}
let regExpFragments = regExpString.split('/');
return new RegExp(regExpFragments[1], regExpFragments[2]);
}
/** Serialize a class symbol information */
function serializeClass(
symbol:ts.Symbol,
decorators:NodeArray<ts.Decorator>
) {
let details = serializeSymbol(symbol);
let properties:DocEntry[] = [];
for (let memberName in symbol.members) {
let member = symbol.members[memberName];
if (member.valueDeclaration) {
if (member.valueDeclaration.kind === SyntaxKind.PropertyDeclaration) {
console.log(`Property: ${memberName}`);
let propertySymbolDescriptor = serializeSymbol(member);
properties.push(propertySymbolDescriptor);
}
} else if (member.declarations) {
console.log('member has declarations');
// FIXME: do we need to support this (constructor, methods)?
} else {
console.log(`member doesn't have valueDeclaration or declarations`);
// FIXME: do we need to support other cases
}
}
details.properties = properties;
// Get the construct signatures
let constructorType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
details.constructors = constructorType.getConstructSignatures().map(serializeSignature);
let classPath = getClassPath(<ts.Node><any>symbol);
// Only top level entities are supported
if (!classPath) {
return details;
}
let parentClassName = getParentClassName(symbol);
let parentClassImport:string;
if (parentClassName) {
parentClassImport = getParentClassImport(<ts.Node><any>symbol, parentClassName);
}
if (isDecoratedAsEntity(decorators)) {
let entityCandidate = EntityCandidate.create(details.name, classPath, parentClassName, parentClassImport);
entityCandidate.docEntry = details;
globalCandidateRegistry.addCandidate(entityCandidate);
processedCandidateRegistry.addCandidate(entityCandidate);
}
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);
}
}