ts2famix
Version:
A TypeScript to JSON importer for Moose.
1,070 lines (890 loc) • 45.2 kB
text/typescript
import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclaration, VariableDeclaration, InterfaceDeclaration, ParameterDeclaration, ConstructorDeclaration, MethodSignature, SourceFile, ModuleDeclaration, PropertyDeclaration, PropertySignature, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ExportedDeclarations, CommentRange, EnumDeclaration, EnumMember, TypeParameterDeclaration, TypeAliasDeclaration, SyntaxKind, FunctionExpression, Block, Identifier, ExpressionWithTypeArguments, ImportDeclaration, Node, ArrowFunction, Scope, ClassExpression } from "ts-morph";
import * as Famix from "../lib/famix/model/famix";
import { calculate } from "../lib/ts-complex/cyclomatic-service";
import * as fs from 'fs';
import { logger, entityDictionary } from "../analyze";
import { getFQN } from "../fqn";
import { InvocableType } from "src/famix_functions/EntityDictionary";
export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember;
export type FamixID = number;
export const methodsAndFunctionsWithId = new Map<number, InvocableType>(); // Maps the Famix method, constructor, getter, setter and function ids to their ts-morph method, constructor, getter, setter or function object
export const accessMap = new Map<FamixID, AccessibleTSMorphElement>(); // Maps the Famix parameter, variable, property and enum value ids to their ts-morph parameter, variable, property or enum member object
export const classes = new Array<ClassDeclaration>(); // Array of all the classes of the source files
export const interfaces = new Array<InterfaceDeclaration>(); // Array of all the interfaces of the source files
export const modules = new Array<SourceFile>(); // Array of all the source files which are modules
export const listOfExportMaps = new Array<ReadonlyMap<string, ExportedDeclarations[]>>(); // Array of all the export maps
export let currentCC: { [key: string]: number }; // Stores the cyclomatic complexity metrics for the current source file
const processedNodesWithTypeParams = new Set<number>(); // Set of nodes that have been processed and have type parameters
/**
* Checks if the file has any imports or exports to be considered a module
* @param sourceFile A source file
* @returns A boolean indicating if the file is a module
*/
function isSourceFileAModule(sourceFile: SourceFile): boolean {
return sourceFile.getImportDeclarations().length > 0 || sourceFile.getExportedDeclarations().size > 0;
}
/**
* Gets the path of a module to be imported
* @param importDecl An import declaration
* @returns The path of the module to be imported
*/
export function getModulePath(importDecl: ImportDeclaration): string {
let path: string;
if (importDecl.getModuleSpecifierSourceFile() === undefined) {
if (importDecl.getModuleSpecifierValue().substring(importDecl.getModuleSpecifierValue().length - 3) === ".ts") {
path = importDecl.getModuleSpecifierValue();
}
else {
path = importDecl.getModuleSpecifierValue() + ".ts";
}
}
else {
path = importDecl.getModuleSpecifierSourceFile()!.getFilePath();
}
return path;
}
/**
* Gets the interfaces implemented or extended by a class or an interface
* @param interfaces An array of interfaces
* @param subClass A class or an interface
* @returns An array of InterfaceDeclaration and ExpressionWithTypeArguments containing the interfaces implemented or extended by the subClass
*/
export function getImplementedOrExtendedInterfaces(interfaces: Array<InterfaceDeclaration>, subClass: ClassDeclaration | InterfaceDeclaration): Array<InterfaceDeclaration | ExpressionWithTypeArguments> {
let impOrExtInterfaces: Array<ExpressionWithTypeArguments>;
if (subClass instanceof ClassDeclaration) {
impOrExtInterfaces = subClass.getImplements();
}
else {
impOrExtInterfaces = subClass.getExtends();
}
const interfacesNames = interfaces.map(i => i.getName());
const implementedOrExtendedInterfaces = new Array<InterfaceDeclaration | ExpressionWithTypeArguments>();
impOrExtInterfaces.forEach(i => {
if (interfacesNames.includes(i.getExpression().getText())) {
implementedOrExtendedInterfaces.push(interfaces[interfacesNames.indexOf(i.getExpression().getText())]);
}
else {
implementedOrExtendedInterfaces.push(i);
}
});
return implementedOrExtendedInterfaces;
}
export function processFiles(sourceFiles: Array<SourceFile>): void {
sourceFiles.forEach(file => {
logger.info(`File: >>>>>>>>>> ${file.getFilePath()}`);
if (fs.existsSync(file.getFilePath())) {
currentCC = calculate(file.getFilePath());
} else {
currentCC = {};
}
processFile(file);
});
}
/**
* Builds a Famix model for a source file
* @param f A source file
*/
function processFile(f: SourceFile): void {
const isModule = isSourceFileAModule(f);
if (isModule) {
modules.push(f);
}
const exportMap = f.getExportedDeclarations();
if (exportMap) listOfExportMaps.push(exportMap);
const fmxFile = entityDictionary.createOrGetFamixFile(f, isModule);
logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`);
processComments(f, fmxFile);
processAliases(f, fmxFile);
processClasses(f, fmxFile);
processInterfaces(f, fmxFile);
processModules(f, fmxFile);
processVariables(f, fmxFile); // This will handle our object literal methods
processEnums(f, fmxFile);
processFunctions(f, fmxFile);
}
export function isAmbient(node: ModuleDeclaration): boolean {
// An ambient module has the DeclareKeyword modifier.
return (node.getModifiers()?.some(modifier => modifier.getKind() === SyntaxKind.DeclareKeyword)) ?? false;
}
export function isNamespace(node: ModuleDeclaration): boolean {
// Check if the module declaration has a namespace keyword.
// This approach uses the getChildren() method to inspect the syntax directly.
return node.getChildrenOfKind(SyntaxKind.NamespaceKeyword).length > 0;
}
/**
* Builds a Famix model for a module (also namespace)
* @param m A namespace
* @returns A Famix.Module representing the module
*/
function processModule(m: ModuleDeclaration): Famix.Module {
const fmxModule = entityDictionary.createOrGetFamixModule(m);
logger.debug(`module: ${m.getName()}, (${m.getType().getText()}), ${fmxModule.fullyQualifiedName}`);
processComments(m, fmxModule);
processAliases(m, fmxModule);
processClasses(m, fmxModule);
processInterfaces(m, fmxModule);
processVariables(m, fmxModule);
processEnums(m, fmxModule);
processFunctions(m, fmxModule);
processModules(m, fmxModule);
return fmxModule;
}
type ContainerTypes = SourceFile | ModuleDeclaration | FunctionDeclaration | FunctionExpression | MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ArrowFunction;
type ScopedTypes = Famix.ScriptEntity | Famix.Module | Famix.Function | Famix.Method | Famix.Accessor;
/**
* Builds a Famix model for the aliases of a container
* @param m A container (a source file, a namespace, a function or a method)
* @param fmxScope The Famix model of the container
*/
function processAliases(m: ContainerTypes, fmxScope: ScopedTypes): void {
logger.debug(`processAliases: ---------- Finding Aliases:`);
m.getTypeAliases().forEach(a => {
const fmxAlias = processAlias(a);
fmxScope.addAlias(fmxAlias);
});
}
/**
* Builds a Famix model for the classes of a container
* @param m A container (a source file or a namespace)
* @param fmxScope The Famix model of the container
*/
function processClasses(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void {
logger.debug(`processClasses: ---------- Finding Classes:`);
const classesInArrowFunctions = getClassesDeclaredInArrowFunctions(m);
const classes = m.getClasses().concat(classesInArrowFunctions);
classes.forEach(c => {
const fmxClass = processClass(c);
fmxScope.addType(fmxClass);
});
}
function getArrowFunctionClasses(f: ArrowFunction): ClassDeclaration[] {
const classes: ClassDeclaration[] = [];
function findClasses(node: Node) {
if (node.getKind() === SyntaxKind.ClassDeclaration) {
classes.push(node as ClassDeclaration);
}
node.getChildren().forEach(findClasses);
}
findClasses(f);
return classes;
}
/**
* ts-morph doesn't find classes in arrow functions, so we need to find them manually
* @param s A source file
* @returns the ClassDeclaration objects found in arrow functions of the source file
*/
function getClassesDeclaredInArrowFunctions(s: SourceFile | ModuleDeclaration): ClassDeclaration[] {
const arrowFunctions = s.getDescendantsOfKind(SyntaxKind.ArrowFunction);
const classesInArrowFunctions = arrowFunctions.map(f => getArrowFunctionClasses(f)).flat();
return classesInArrowFunctions;
}
/**
* Builds a Famix model for the interfaces of a container
* @param m A container (a source file or a namespace)
* @param fmxScope The Famix model of the container
*/
function processInterfaces(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void {
logger.debug(`processInterfaces: ---------- Finding Interfaces:`);
m.getInterfaces().forEach(i => {
const fmxInterface = processInterface(i);
fmxScope.addType(fmxInterface);
});
}
/**
* Builds a Famix model for the variables of a container
* @param m A container (a source file, a namespace, a function or a method)
* @param fmxScope The Famix model of the container
*/
function processVariables(m: ContainerTypes, fmxScope: Famix.ScriptEntity | Famix.Module | Famix.Function | Famix.Method | Famix.Accessor): void {
logger.debug(`processVariables: ---------- Finding Variables:`);
m.getVariableStatements().forEach(v => {
const fmxVariables = processVariableStatement(v);
fmxVariables.forEach(fmxVariable => {
fmxScope.addVariable(fmxVariable);
});
// Check each VariableDeclaration for object literal methods
v.getDeclarations().forEach(varDecl => {
const varName = varDecl.getName();
console.log(`Checking variable: ${varName} at pos=${varDecl.getStart()}`);
const initializer = varDecl.getInitializer();
if (initializer && Node.isObjectLiteralExpression(initializer)) {
initializer.getProperties().forEach(prop => {
if (Node.isPropertyAssignment(prop)) {
const nested = prop.getInitializer();
if (nested && Node.isObjectLiteralExpression(nested)) {
nested.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => {
console.log(`Found object literal method: ${method.getName()} at pos=${method.getStart()}`);
entityDictionary.createOrGetFamixMethod(method, currentCC);
});
}
}
});
}
});
});
}
/**
* Builds a Famix model for the enums of a container
* @param m A container (a source file, a namespace, a function or a method)
* @param fmxScope The Famix model of the container
*/
function processEnums(m: ContainerTypes, fmxScope: ScopedTypes): void {
logger.debug(`processEnums: ---------- Finding Enums:`);
m.getEnums().forEach(e => {
const fmxEnum = processEnum(e);
fmxScope.addType(fmxEnum);
});
}
/**
* Builds a Famix model for the functions of a container
* @param m A container (a source file, a namespace, a function or a method)
* @param fmxScope The Famix model of the container
*/
function processFunctions(m: ContainerTypes, fmxScope: ScopedTypes): void {
logger.debug(`Finding Functions:`);
m.getFunctions().forEach(f => {
const fmxFunction = processFunction(f);
fmxScope.addFunction(fmxFunction);
});
//find arrow functions
logger.debug(`Finding Functions:`);
const arrowFunctions = m.getDescendantsOfKind(SyntaxKind.ArrowFunction);
arrowFunctions.forEach(af => {
const fmxFunction = processFunction(af);
fmxScope.addFunction(fmxFunction);
});
}
/**
* Builds a Famix model for the modules of a container.
* @param m A container (a source file or a namespace)
* @param fmxScope The Famix model of the container
*/
function processModules(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void {
logger.debug(`Finding Modules:`);
m.getModules().forEach(md => {
const fmxModule = processModule(md);
fmxScope.addModule(fmxModule);
});
}
/**
* Builds a Famix model for an alias
* @param a An alias
* @returns A Famix.Alias representing the alias
*/
function processAlias(a: TypeAliasDeclaration): Famix.Alias {
const fmxAlias = entityDictionary.createFamixAlias(a);
logger.debug(`Alias: ${a.getName()}, (${a.getType().getText()}), fqn = ${fmxAlias.fullyQualifiedName}`);
processComments(a, fmxAlias);
return fmxAlias;
}
/**
* Builds a Famix model for a class
* @param c A class
* @returns A Famix.Class or a Famix.ParametricClass representing the class
*/
function processClass(c: ClassDeclaration): Famix.Class | Famix.ParametricClass {
classes.push(c);
const fmxClass = entityDictionary.createOrGetFamixClass(c);
logger.debug(`Class: ${c.getName()}, (${c.getType().getText()}), fqn = ${fmxClass.fullyQualifiedName}`);
processComments(c, fmxClass);
processDecorators(c, fmxClass);
processStructuredType(c, fmxClass);
c.getConstructors().forEach(con => {
const fmxCon = processMethod(con);
fmxClass.addMethod(fmxCon);
});
c.getGetAccessors().forEach(acc => {
const fmxAcc = processMethod(acc);
fmxClass.addMethod(fmxAcc);
});
c.getSetAccessors().forEach(acc => {
const fmxAcc = processMethod(acc);
fmxClass.addMethod(fmxAcc);
});
return fmxClass;
}
/**
* Builds a Famix model for an interface
* @param i An interface
* @returns A Famix.Interface or a Famix.ParametricInterface representing the interface
*/
function processInterface(i: InterfaceDeclaration): Famix.Interface | Famix.ParametricInterface {
interfaces.push(i);
const fmxInterface = entityDictionary.createOrGetFamixInterface(i);
logger.debug(`Interface: ${i.getName()}, (${i.getType().getText()}), fqn = ${fmxInterface.fullyQualifiedName}`);
processComments(i, fmxInterface);
processStructuredType(i, fmxInterface);
return fmxInterface;
}
/**
* Builds a Famix model for the type parameters, properties and methods of a structured type
* @param c A structured type (a class or an interface)
* @param fmxScope The Famix model of the structured type
*/
function processStructuredType(c: ClassDeclaration | InterfaceDeclaration, fmxScope: Famix.Class | Famix.ParametricClass | Famix.Interface | Famix.ParametricInterface): void {
logger.debug(`Finding Properties and Methods:`);
if (fmxScope instanceof Famix.ParametricClass || fmxScope instanceof Famix.ParametricInterface) {
processTypeParameters(c, fmxScope);
}
c.getProperties().forEach(prop => {
const fmxProperty = processProperty(prop);
fmxScope.addProperty(fmxProperty);
});
c.getMethods().forEach(m => {
const fmxMethod = processMethod(m);
fmxScope.addMethod(fmxMethod);
});
}
/**
* Builds a Famix model for a property
* @param p A property
* @returns A Famix.Property representing the property
*/
function processProperty(p: PropertyDeclaration | PropertySignature): Famix.Property {
const fmxProperty = entityDictionary.createFamixProperty(p);
logger.debug(`property: ${p.getName()}, (${p.getType().getText()}), fqn = ${fmxProperty.fullyQualifiedName}`);
logger.debug(` ---> It's a Property${(p instanceof PropertySignature) ? "Signature" : "Declaration"}!`);
const ancestor = p.getFirstAncestorOrThrow();
logger.debug(` ---> Its first ancestor is a ${ancestor.getKindName()}`);
// decorators
if (!(p instanceof PropertySignature)) {
processDecorators(p, fmxProperty);
// only add access if the p's first ancestor is not a PropertyDeclaration
if (ancestor.getKindName() !== "PropertyDeclaration") {
logger.debug(`adding access to map: ${p.getName()}, (${p.getType().getText()}) Famix ${fmxProperty.name} id: ${fmxProperty.id}`);
accessMap.set(fmxProperty.id, p);
}
}
processComments(p, fmxProperty);
return fmxProperty;
}
/**
* Builds a Famix model for a method or an accessor
* @param m A method or an accessor
* @returns A Famix.Method or a Famix.Accessor representing the method or the accessor
*/
function processMethod(m: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration): Famix.Method | Famix.Accessor {
const fmxMethod = entityDictionary.createOrGetFamixMethod(m, currentCC);
logger.debug(`Method: ${!(m instanceof ConstructorDeclaration) ? m.getName() : "constructor"}, (${m.getType().getText()}), parent: ${(m.getParent() as ClassDeclaration | InterfaceDeclaration).getName()}, fqn = ${fmxMethod.fullyQualifiedName}`);
processComments(m, fmxMethod);
processTypeParameters(m, fmxMethod);
processParameters(m, fmxMethod);
if (!(m instanceof MethodSignature)) {
processAliases(m, fmxMethod);
processVariables(m, fmxMethod);
processEnums(m, fmxMethod);
processFunctions(m, fmxMethod);
processFunctionExpressions(m, fmxMethod);
methodsAndFunctionsWithId.set(fmxMethod.id, m);
}
if (m instanceof MethodDeclaration || m instanceof GetAccessorDeclaration || m instanceof SetAccessorDeclaration) {
processDecorators(m, fmxMethod);
}
return fmxMethod;
}
/**
* Builds a Famix model for a function
* @param f A function
* @returns A Famix.Function representing the function
*/
function processFunction(f: FunctionDeclaration | FunctionExpression | ArrowFunction): Famix.Function {
logger.debug(`Function: ${(f instanceof ArrowFunction ? "anonymous" : f.getName() ? f.getName() : "anonymous")}, (${f.getType().getText()}), fqn = ${getFQN(f)}`);
let fmxFunction;
if (f instanceof ArrowFunction) {
fmxFunction = entityDictionary.createOrGetFamixArrowFunction(f, currentCC);
} else {
fmxFunction = entityDictionary.createOrGetFamixFunction(f, currentCC);
}
processComments(f, fmxFunction);
processAliases(f, fmxFunction);
processTypeParameters(f, fmxFunction);
processParameters(f, fmxFunction);
processVariables(f, fmxFunction);
processEnums(f, fmxFunction);
processFunctions(f, fmxFunction);
if (f instanceof FunctionDeclaration && !(f.getParent() instanceof Block)) {
processFunctionExpressions(f, fmxFunction);
}
methodsAndFunctionsWithId.set(fmxFunction.id, f);
return fmxFunction;
}
/**
* Builds a Famix model for the function expressions of a function or a method
* @param f A function or a method
* @param fmxScope The Famix model of the function or the method
*/
function processFunctionExpressions(f: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration, fmxScope: Famix.Function | Famix.Method | Famix.Accessor): void {
logger.debug(`Finding Function Expressions:`);
const functionExpressions = f.getDescendantsOfKind(SyntaxKind.FunctionExpression);
functionExpressions.forEach((func) => {
const fmxFunc = processFunction(func);
fmxScope.addFunction(fmxFunc);
});
}
/**
* Builds a Famix model for the parameters of a method or a function
* @param m A method or a function
* @param fmxScope The Famix model of the method or the function
*/
function processParameters(m: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ArrowFunction, fmxScope: Famix.Method | Famix.Accessor | Famix.Function): void {
logger.debug(`Finding Parameters:`);
m.getParameters().forEach(param => {
const fmxParam = processParameter(param);
fmxScope.addParameter(fmxParam);
// Additional handling for Parameter Properties in constructors
if (m instanceof ConstructorDeclaration) {
// Check if the parameter has any visibility modifier
if (param.hasModifier(SyntaxKind.PrivateKeyword) || param.hasModifier(SyntaxKind.PublicKeyword) || param.hasModifier(SyntaxKind.ProtectedKeyword) || param.hasModifier(SyntaxKind.ReadonlyKeyword)) {
const classOfConstructor = m.getParent();
logger.info(`Parameter Property ${param.getName()} in constructor of ${classOfConstructor.getName()}.`);
// Treat the parameter as a property and add it to the class
const fmxProperty = processParameterAsProperty(param, classOfConstructor);
fmxProperty.readOnly = param.hasModifier(SyntaxKind.ReadonlyKeyword);
}
}
});
}
// This function should create a Famix.Property model from a ParameterDeclaration
// You'll need to implement it according to your Famix model structure
function processParameterAsProperty(param: ParameterDeclaration, classDecl: ClassDeclaration | ClassExpression): Famix.Property {
// Convert the parameter into a Property
const propertyRepresentation = convertParameterToPropertyRepresentation(param);
// Add the property to the class so we can have a PropertyDeclaration object
classDecl.addProperty(propertyRepresentation);
const property = classDecl.getProperty(propertyRepresentation.name);
if (!property) {
throw new Error(`Property ${propertyRepresentation.name} not found in class ${classDecl.getName()}`);
}
const fmxProperty = entityDictionary.createFamixProperty(property);
if (classDecl instanceof ClassDeclaration) {
const fmxClass = entityDictionary.createOrGetFamixClass(classDecl);
fmxClass.addProperty(fmxProperty);
} else {
throw new Error("Unexpected type ClassExpression.");
}
processComments(property, fmxProperty);
// remove the property from the class
property.remove();
return fmxProperty;
}
function convertParameterToPropertyRepresentation(param: ParameterDeclaration) {
// Extract name
const paramName = param.getName();
// Extract type
const paramType = param.getType().getText(param);
// Determine visibility
let scope: Scope;
if (param.hasModifier(SyntaxKind.PrivateKeyword)) {
scope = Scope.Private;
} else if (param.hasModifier(SyntaxKind.ProtectedKeyword)) {
scope = Scope.Protected;
} else if (param.hasModifier(SyntaxKind.PublicKeyword)) {
scope = Scope.Public;
} else {
throw new Error(`Parameter property ${paramName} in constructor does not have a visibility modifier.`);
}
// Determine if readonly
const isReadonly = param.hasModifier(SyntaxKind.ReadonlyKeyword);
// Create a representation of the property
const propertyRepresentation = {
name: paramName,
type: paramType,
scope: scope,
isReadonly: isReadonly,
};
return propertyRepresentation;
}
/**
* Builds a Famix model for a parameter
* @param paramDecl A parameter
* @returns A Famix.Parameter representing the parameter
*/
function processParameter(paramDecl: ParameterDeclaration): Famix.Parameter {
const fmxParam = entityDictionary.createOrGetFamixParameter(paramDecl); // create or GET
logger.debug(`parameter: ${paramDecl.getName()}, (${paramDecl.getType().getText()}), fqn = ${fmxParam.fullyQualifiedName}`);
processComments(paramDecl, fmxParam);
processDecorators(paramDecl, fmxParam);
const parent = paramDecl.getParent();
if (!(parent instanceof MethodSignature)) {
logger.debug(`adding access: ${paramDecl.getName()}, (${paramDecl.getType().getText()}) Famix ${fmxParam.name}`);
accessMap.set(fmxParam.id, paramDecl);
}
return fmxParam;
}
function processTypeParameters(
e: ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ArrowFunction,
fmxScope: Famix.ParametricClass | Famix.ParametricInterface | Famix.Method | Famix.Accessor | Famix.Function | Famix.ArrowFunction
): void {
logger.debug(`Finding Type Parameters:`);
const nodeStart = e.getStart();
// Check if this node has already been processed
if (processedNodesWithTypeParams.has(nodeStart)) {
return;
}
// Get type parameters
const typeParams = e.getTypeParameters();
// Process each type parameter
typeParams.forEach((tp) => {
const fmxParam = processTypeParameter(tp);
fmxScope.addGenericParameter(fmxParam);
});
// Log if no type parameters were found
if (typeParams.length === 0) {
logger.debug(`[processTypeParameters] No type parameters found for this node`);
}
// Mark this node as processed
processedNodesWithTypeParams.add(nodeStart);
}
/**
* Builds a Famix model for a type parameter
* @param tp A type parameter
* @returns A Famix.TypeParameter representing the type parameter
*/
function processTypeParameter(tp: TypeParameterDeclaration): Famix.ParameterType {
const fmxTypeParameter = entityDictionary.createFamixParameterType(tp);
logger.debug(`type parameter: ${tp.getName()}, (${tp.getType().getText()}), fqn = ${fmxTypeParameter.fullyQualifiedName}`);
processComments(tp, fmxTypeParameter);
return fmxTypeParameter;
}
/**
* Builds a Famix model for the variables of a variable statement
* @param v A variable statement
* @returns An array of Famix.Variable representing the variables
*/
function processVariableStatement(v: VariableStatement): Array<Famix.Variable> {
const fmxVariables = new Array<Famix.Variable>();
logger.debug(`Variable statement: ${v.getText()}, (${v.getType().getText()}), ${v.getDeclarationKindKeywords()[0]}, fqn = ${v.getDeclarations()[0].getName()}`);
v.getDeclarations().forEach(variable => {
const fmxVar = processVariable(variable);
processComments(v, fmxVar);
fmxVariables.push(fmxVar);
});
return fmxVariables;
}
/**
* Builds a Famix model for a variable
* @param v A variable
* @returns A Famix.Variable representing the variable
*/
function processVariable(v: VariableDeclaration): Famix.Variable {
const fmxVar = entityDictionary.createOrGetFamixVariable(v);
logger.debug(`variable: ${v.getName()}, (${v.getType().getText()}), ${v.getInitializer() ? "initializer: " + v.getInitializer()!.getText() : "initializer: "}, fqn = ${fmxVar.fullyQualifiedName}`);
processComments(v, fmxVar);
logger.debug(`adding access: ${v.getName()}, (${v.getType().getText()}) Famix ${fmxVar.name}`);
accessMap.set(fmxVar.id, v);
return fmxVar;
}
/**
* Builds a Famix model for an enum
* @param e An enum
* @returns A Famix.Enum representing the enum
*/
function processEnum(e: EnumDeclaration): Famix.Enum {
const fmxEnum = entityDictionary.createOrGetFamixEnum(e);
logger.debug(`enum: ${e.getName()}, (${e.getType().getText()}), fqn = ${fmxEnum.fullyQualifiedName}`);
processComments(e, fmxEnum);
e.getMembers().forEach(m => {
const fmxEnumValue = processEnumValue(m);
fmxEnum.addValue(fmxEnumValue);
});
return fmxEnum;
}
/**
* Builds a Famix model for an enum member
* @param v An enum member
* @returns A Famix.EnumValue representing the enum member
*/
function processEnumValue(v: EnumMember): Famix.EnumValue {
const fmxEnumValue = entityDictionary.createFamixEnumValue(v);
logger.debug(`enum value: ${v.getName()}, (${v.getType().getText()}), fqn = ${fmxEnumValue.fullyQualifiedName}`);
processComments(v, fmxEnumValue);
logger.debug(`adding access: ${v.getName()}, (${v.getType().getText()}) Famix ${fmxEnumValue.name}`);
accessMap.set(fmxEnumValue.id, v);
return fmxEnumValue;
}
/**
* Builds a Famix model for the decorators of a class, a method, a parameter or a property
* @param e A class, a method, a parameter or a property
* @param fmxScope The Famix model of the class, the method, the parameter or the property
*/
function processDecorators(e: ClassDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ParameterDeclaration | PropertyDeclaration, fmxScope: Famix.Class | Famix.ParametricClass | Famix.Method | Famix.Accessor | Famix.Parameter | Famix.Property): void {
logger.debug(`Finding Decorators:`);
e.getDecorators().forEach(dec => {
const fmxDec = processDecorator(dec, e);
fmxScope.addDecorator(fmxDec);
});
}
/**
* Builds a Famix model for a decorator
* @param d A decorator
* @param e A class, a method, a parameter or a property
* @returns A Famix.Decorator representing the decorator
*/
function processDecorator(d: Decorator, e: ClassDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ParameterDeclaration | PropertyDeclaration): Famix.Decorator {
const fmxDec = entityDictionary.createOrGetFamixDecorator(d, e);
logger.debug(`decorator: ${d.getName()}, (${d.getType().getText()}), fqn = ${fmxDec.fullyQualifiedName}`);
processComments(d, fmxDec);
return fmxDec;
}
/**
* Builds a Famix model for the comments
* @param e A ts-morph element
* @param fmxScope The Famix model of the named entity
*/
function processComments(e: SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | Decorator | EnumDeclaration | EnumMember | TypeParameterDeclaration | VariableStatement | TypeAliasDeclaration | ArrowFunction, fmxScope: Famix.NamedEntity): void {
logger.debug(`Process comments:`);
e.getLeadingCommentRanges().forEach(c => {
const fmxComment = processComment(c, fmxScope);
logger.debug(`leading comments, addComment: '${c.getText()}'`);
fmxScope.addComment(fmxComment); // redundant, but just in case
});
e.getTrailingCommentRanges().forEach(c => {
const fmxComment = processComment(c, fmxScope);
logger.debug(`trailing comments, addComment: '${c.getText()}'`);
fmxScope.addComment(fmxComment);
});
}
/**
* Builds a Famix model for a comment
* @param c A comment
* @param fmxScope The Famix model of the comment's container
* @returns A Famix.Comment representing the comment
*/
function processComment(c: CommentRange, fmxScope: Famix.NamedEntity): Famix.Comment {
const isJSDoc = c.getText().startsWith("/**");
logger.debug(`processComment: comment: ${c.getText()}, isJSDoc = ${isJSDoc}`);
const fmxComment = entityDictionary.createFamixComment(c, fmxScope, isJSDoc);
return fmxComment;
}
/**
* Builds a Famix model for the accesses on the parameters, variables, properties and enum members of the source files
* @param accessMap A map of parameters, variables, properties and enum members with their id
*/
export function processAccesses(accessMap: Map<FamixID, AccessibleTSMorphElement>): void {
logger.debug(`Creating accesses:`);
accessMap.forEach((v, id) => {
logger.debug(`Accesses to ${v.getName()}`);
// try {
const temp_nodes = v.findReferencesAsNodes() as Array<Identifier>;
temp_nodes.forEach(node => processNodeForAccesses(node, id));
// } catch (error) {
// logger.error(`> WARNING: got exception "${error}".\nContinuing...`);
// }
});
}
/**
* Builds a Famix model for an access on a parameter, variable, property or enum member
* @param n A node
* @param id An id of a parameter, a variable, a property or an enum member
*/
function processNodeForAccesses(n: Identifier, id: number): void {
// try {
// sometimes node's first ancestor is a PropertyDeclaration, which is not an access
// see https://github.com/fuhrmanator/FamixTypeScriptImporter/issues/9
// check for a node whose first ancestor is a property declaration and bail?
// This may be a bug in ts-morph?
if (n.getFirstAncestorOrThrow().getKindName() === "PropertyDeclaration") {
logger.debug(`processNodeForAccesses: node kind: ${n.getKindName()}, ${n.getText()}, (${n.getType().getText()})'s first ancestor is a PropertyDeclaration. Skipping...`);
return;
}
entityDictionary.createFamixAccess(n, id);
logger.debug(`processNodeForAccesses: node kind: ${n.getKindName()}, ${n.getText()}, (${n.getType().getText()})`);
// } catch (error) {
// logger.error(`> Got exception "${error}".\nScopeDeclaration invalid for "${n.getSymbol().fullyQualifiedName}".\nContinuing...`);
// }
}
// exports has name -> Declaration -- the declaration can be used to find the FamixElement
// handle `import path = require("path")` for example
export function processImportClausesForImportEqualsDeclarations(sourceFiles: Array<SourceFile>, exports: Array<ReadonlyMap<string, ExportedDeclarations[]>>): void {
logger.info(`Creating import clauses from ImportEqualsDeclarations in source files:`);
sourceFiles.forEach(sourceFile => {
sourceFile.forEachDescendant(node => {
if (Node.isImportEqualsDeclaration(node)) {
// You've found an ImportEqualsDeclaration
logger.info("Declaration Name:", node.getName());
logger.info("Module Reference Text:", node.getModuleReference().getText());
// what's the name of the imported entity?
// const importedEntity = node.getName();
// create a famix import clause
const namedImport = node.getNameNode();
entityDictionary.oldCreateOrGetFamixImportClause({
importDeclaration: node,
importerSourceFile: sourceFile,
moduleSpecifierFilePath: node.getModuleReference().getText(),
importElement: namedImport,
isInExports: exports.find(e => e.has(namedImport.getText())) !== undefined,
isDefaultExport: false
});
// entityDictionary.createFamixImportClause(importedEntity, importingEntity);
}
});
}
);
}
/**
* Builds a Famix model for the import clauses of the source files which are modules
* @param modules An array of modules
* @param exports An array of maps of exported declarations
*/
export function processImportClausesForModules(modules: Array<SourceFile>, exports: Array<ReadonlyMap<string, ExportedDeclarations[]>>): void {
logger.info(`Creating import clauses from ${modules.length} modules:`);
modules.forEach(module => {
const modulePath = module.getFilePath();
module.getImportDeclarations().forEach(impDecl => {
logger.info(`Importing ${impDecl.getModuleSpecifierValue()} in ${modulePath}`);
const path = getModulePath(impDecl);
impDecl.getNamedImports().forEach(namedImport => {
logger.info(`Importing (named) ${namedImport.getName()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`);
const importedEntityName = namedImport.getName();
const importFoundInExports = isInExports(exports, importedEntityName);
logger.debug(`Processing ImportSpecifier: ${namedImport.getText()}, pos=${namedImport.getStart()}`);
entityDictionary.oldCreateOrGetFamixImportClause({
importDeclaration: impDecl,
importerSourceFile: module,
moduleSpecifierFilePath: path,
importElement: namedImport,
isInExports: importFoundInExports,
isDefaultExport: false
});
});
const defaultImport = impDecl.getDefaultImport();
if (defaultImport !== undefined) {
logger.info(`Importing (default) ${defaultImport.getText()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`);
logger.debug(`Processing Default Import: ${defaultImport.getText()}, pos=${defaultImport.getStart()}`);
entityDictionary.oldCreateOrGetFamixImportClause({
importDeclaration: impDecl,
importerSourceFile: module,
moduleSpecifierFilePath: path,
importElement: defaultImport,
isInExports: false,
isDefaultExport: true
});
}
const namespaceImport = impDecl.getNamespaceImport();
if (namespaceImport !== undefined) {
logger.info(`Importing (namespace) ${namespaceImport.getText()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`);
entityDictionary.oldCreateOrGetFamixImportClause({
importDeclaration: impDecl,
importerSourceFile: module,
moduleSpecifierFilePath: path,
importElement: namespaceImport,
isInExports: false,
isDefaultExport: false
});
}
});
});
}
function isInExports(exports: ReadonlyMap<string, ExportedDeclarations[]>[], importedEntityName: string) {
let importFoundInExports = false;
exports.forEach(e => {
if (e.has(importedEntityName)) {
importFoundInExports = true;
}
});
return importFoundInExports;
}
/**
* Builds a Famix model for the inheritances of the classes and interfaces of the source files
* @param classes An array of classes
* @param interfaces An array of interfaces
*/
export function processInheritances(classes: ClassDeclaration[], interfaces: InterfaceDeclaration[]): void {
logger.info(`Creating inheritances:`);
classes.forEach(cls => {
logger.debug(`Checking class inheritance for ${cls.getName()}`);
const baseClass = cls.getBaseClass();
if (baseClass !== undefined) {
entityDictionary.createOrGetFamixInheritance(cls, baseClass);
logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), extClass: ${baseClass.getName()}, (${baseClass.getType().getText()})`);
} // this is false when the class extends an undefined class
else {
// check for "extends" of unresolved class
const undefinedExtendedClass = cls.getExtends();
if (undefinedExtendedClass) {
entityDictionary.createOrGetFamixInheritance(cls, undefinedExtendedClass);
logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), undefinedExtendedClass: ${undefinedExtendedClass.getText()}`);
}
}
logger.debug(`Checking interface inheritance for ${cls.getName()}`);
const implementedInterfaces = getImplementedOrExtendedInterfaces(interfaces, cls);
implementedInterfaces.forEach(implementedIF => {
entityDictionary.createOrGetFamixInheritance(cls, implementedIF);
logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), impInter: ${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getName() : implementedIF.getExpression().getText()}, (${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getType().getText() : implementedIF.getExpression().getText()})`);
});
});
interfaces.forEach(interFace => {
try {
logger.debug(`Checking interface inheritance for ${interFace.getName()}`);
const extendedInterfaces = getImplementedOrExtendedInterfaces(interfaces, interFace);
extendedInterfaces.forEach(extendedInterface => {
entityDictionary.createOrGetFamixInheritance(interFace, extendedInterface);
logger.debug(`interFace: ${interFace.getName()}, (${interFace.getType().getText()}), extendedInterface: ${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getName() : extendedInterface.getExpression().getText()}, (${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getType().getText() : extendedInterface.getExpression().getText()})`);
});
}
catch (error) {
logger.error(`> WARNING: got exception ${error}. Continuing...`);
}
});
}
/**
* Builds a Famix model for the invocations of the methods and functions of the source files
* @param methodsAndFunctionsWithId A map of methods and functions with their id
*/
export function processInvocations(methodsAndFunctionsWithId: Map<number, InvocableType>): void {
logger.info(`Creating invocations:`);
methodsAndFunctionsWithId.forEach((invocable, id) => {
if (!(invocable instanceof ArrowFunction)) { // ArrowFunctions are not directly invoked
logger.debug(`Invocations to ${(invocable instanceof MethodDeclaration || invocable instanceof GetAccessorDeclaration || invocable instanceof SetAccessorDeclaration || invocable instanceof FunctionDeclaration) ? invocable.getName() : ((invocable instanceof ConstructorDeclaration) ? 'constructor' : (invocable.getName() ? invocable.getName() : 'anonymous'))} (${invocable.getType().getText()})`);
try {
const nodesReferencingInvocable = invocable.findReferencesAsNodes() as Array<Identifier>;
nodesReferencingInvocable.forEach(
nodeReferencingInvocable => processNodeForInvocations(nodeReferencingInvocable, invocable, id));
} catch (error) {
logger.error(`> WARNING: got exception ${error}. Continuing...`);
}
} else {
logger.debug(`Skipping invocation to ArrowFunction: ${(invocable.getBodyText())}`);
}
});
}
/**
* Builds a Famix model for an invocation of a method or a function
* @param nodeReferencingInvocable A node
* @param invocable A method or a function
* @param id The id of the method or the function
*/
function processNodeForInvocations(nodeReferencingInvocable: Identifier, invocable: InvocableType, id: number): void {
try {
entityDictionary.createFamixInvocation(nodeReferencingInvocable, invocable, id);
logger.debug(`node: ${nodeReferencingInvocable.getKindName()}, (${nodeReferencingInvocable.getType().getText()})`);
} catch (error) {
logger.error(`> WARNING: got exception ${error}. ScopeDeclaration invalid for ${nodeReferencingInvocable.getSymbol()!.getFullyQualifiedName()}. Continuing...`);
}
}
/**
* Builds a Famix model for the inheritances of the classes and interfaces of the source files
* @param classes An array of classes
* @param interfaces An array of interfaces
*/
export function processConcretisations(classes: ClassDeclaration[], interfaces: InterfaceDeclaration[], functions: Map<number, MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ArrowFunction>): void {
logger.info(`processConcretisations: Creating concretisations:`);
classes.forEach(cls => {
logger.debug(`processConcretisations: Checking class concretisation for ${cls.getName()}`);
entityDictionary.createFamixConcretisationClassOrInterfaceSpecialisation(cls);
entityDictionary.createFamixConcretisationGenericInstantiation(cls);
entityDictionary.createFamixConcretisationInterfaceClass(cls);
entityDictionary.createFamixConcretisationTypeInstanciation(cls);
});
interfaces.forEach(inter => {
logger.debug(`processConcretisations: Checking interface concretisation for ${inter.getName()}`);
entityDictionary.createFamixConcretisationTypeInstanciation(inter);
entityDictionary.createFamixConcretisationClassOrInterfaceSpecialisation(inter);
});
functions.forEach(func => {
if(func instanceof FunctionDeclaration || func instanceof MethodDeclaration ){
logger.debug(`processConcretisations: Checking Method concretisation`);
entityDictionary.createFamixConcretisationFunctionInstantiation(func);
}
});
}