diff --git a/src/analyze.ts b/src/analyze.ts index eee81f4a..dd14e225 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -2,18 +2,18 @@ import { Project } from "ts-morph"; import * as fs from 'fs'; import { FamixRepository } from "./lib/famix/famix_repository"; import { Logger } from "tslog"; -import * as processFunctions from "./analyze_functions/process_functions"; -import { EntityDictionary } from "./famix_functions/EntityDictionary"; +import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/EntityDictionary"; import path from "path"; +import { TypeScriptToFamixProcessor } from "./analyze_functions/process_functions"; export const logger = new Logger({ name: "ts2famix", minLevel: 2 }); -export const config = { "expectGraphemes": false }; -export const entityDictionary = new EntityDictionary(); /** * This class is used to build a Famix model from a TypeScript source code */ export class Importer { + private entityDictionary: EntityDictionary; + private processFunctions: TypeScriptToFamixProcessor ; private project = new Project( { @@ -23,6 +23,11 @@ export class Importer { } ); // The project containing the source files to analyze + constructor(config: EntityDictionaryConfig = { expectGraphemes: false }) { + this.entityDictionary = new EntityDictionary(config); + this.processFunctions = new TypeScriptToFamixProcessor (this.entityDictionary); + } + /** * Main method * @param paths An array of paths to the source files to analyze @@ -35,11 +40,11 @@ export class Importer { this.project.addSourceFilesAtPaths(paths); - initFamixRep(this.project); + this.initFamixRep(this.project); this.processEntities(this.project); - const famixRep = entityDictionary.famixRep; + const famixRep = this.entityDictionary.famixRep; // } // catch (error) { // logger.error(`> ERROR: got exception ${error}. Exiting...`); @@ -53,20 +58,20 @@ export class Importer { private processEntities(project: Project): void { const onlyTypeScriptFiles = project.getSourceFiles().filter(f => f.getFilePath().endsWith('.ts')); - processFunctions.processFiles(onlyTypeScriptFiles); - const accesses = processFunctions.accessMap; - const methodsAndFunctionsWithId = processFunctions.methodsAndFunctionsWithId; - const classes = processFunctions.classes; - const interfaces = processFunctions.interfaces; - const modules = processFunctions.modules; - const exports = processFunctions.listOfExportMaps; - - processFunctions.processImportClausesForImportEqualsDeclarations(project.getSourceFiles(), exports); - processFunctions.processImportClausesForModules(modules, exports); - processFunctions.processAccesses(accesses); - processFunctions.processInvocations(methodsAndFunctionsWithId); - processFunctions.processInheritances(classes, interfaces); - processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId); + this.processFunctions.processFiles(onlyTypeScriptFiles); + const accesses = this.processFunctions.accessMap; + const methodsAndFunctionsWithId = this.processFunctions.methodsAndFunctionsWithId; + const classes = this.processFunctions.classes; + const interfaces = this.processFunctions.interfaces; + const modules = this.processFunctions.modules; + const exports = this.processFunctions.listOfExportMaps; + + this.processFunctions.processImportClausesForImportEqualsDeclarations(project.getSourceFiles(), exports); + this.processFunctions.processImportClausesForModules(modules, exports); + this.processFunctions.processAccesses(accesses); + this.processFunctions.processInvocations(methodsAndFunctionsWithId); + this.processFunctions.processInheritances(classes, interfaces); + this.processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId); } @@ -98,23 +103,22 @@ export class Importer { //const famixRep = this.famixRepFromPaths(sourceFileNames); - initFamixRep(project); + this.initFamixRep(project); this.processEntities(project); - return entityDictionary.famixRep; + return this.entityDictionary.famixRep; } -} - -function initFamixRep(project: Project): void { - // get compiler options - const compilerOptions = project.getCompilerOptions(); - - // get baseUrl - const baseUrl = compilerOptions.baseUrl || "."; - - const absoluteBaseUrl = path.resolve(baseUrl); - - entityDictionary.famixRep.setAbsolutePath(path.normalize(absoluteBaseUrl)); + private initFamixRep(project: Project): void { + // get compiler options + const compilerOptions = project.getCompilerOptions(); + + // get baseUrl + const baseUrl = compilerOptions.baseUrl || "."; + + const absoluteBaseUrl = path.resolve(baseUrl); + + this.entityDictionary.setAbsolutePath(path.normalize(absoluteBaseUrl)); + } } diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 8c977746..e4d3afc5 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -2,22 +2,16 @@ import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclara 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 { logger } from "../analyze"; import { getFQN } from "../fqn"; -import { InvocableType } from "src/famix_functions/EntityDictionary"; +import { EntityDictionary, InvocableType } from "src/famix_functions/EntityDictionary"; export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember; export type FamixID = number; + +type ContainerTypes = SourceFile | ModuleDeclaration | FunctionDeclaration | FunctionExpression | MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ArrowFunction; -export const methodsAndFunctionsWithId = new Map(); // 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(); // 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(); // Array of all the classes of the source files -export const interfaces = new Array(); // Array of all the interfaces of the source files -export const modules = new Array(); // Array of all the source files which are modules -export const listOfExportMaps = new Array>(); // 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(); // Set of nodes that have been processed and have type parameters +type ScopedTypes = Famix.ScriptEntity | Famix.Module | Famix.Function | Famix.Method | Famix.Accessor; /** * Checks if the file has any imports or exports to be considered a module @@ -28,1042 +22,1055 @@ 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; -} +export class TypeScriptToFamixProcessor { + private entityDictionary: EntityDictionary; + public methodsAndFunctionsWithId = new Map(); // Maps the Famix method, constructor, getter, setter and function ids to their ts-morph method, constructor, getter, setter or function object + + public accessMap = new Map(); // Maps the Famix parameter, variable, property and enum value ids to their ts-morph parameter, variable, property or enum member object + public classes = new Array(); // Array of all the classes of the source files + public interfaces = new Array(); // Array of all the interfaces of the source files + public modules = new Array(); // Array of all the source files which are modules + public listOfExportMaps = new Array>(); // Array of all the export maps + private processedNodesWithTypeParams = new Set(); // Set of nodes that have been processed and have type parameters -/** - * 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, subClass: ClassDeclaration | InterfaceDeclaration): Array { - let impOrExtInterfaces: Array; - if (subClass instanceof ClassDeclaration) { - impOrExtInterfaces = subClass.getImplements(); - } - else { - impOrExtInterfaces = subClass.getExtends(); - } + private currentCC: { [key: string]: number }; // Stores the cyclomatic complexity metrics for the current source file - const interfacesNames = interfaces.map(i => i.getName()); - const implementedOrExtendedInterfaces = new Array(); + constructor(entityDictionary: EntityDictionary) { + this.entityDictionary = entityDictionary; + this.currentCC = {}; + } - impOrExtInterfaces.forEach(i => { - if (interfacesNames.includes(i.getExpression().getText())) { - implementedOrExtendedInterfaces.push(interfaces[interfacesNames.indexOf(i.getExpression().getText())]); + /** + * Gets the path of a module to be imported + * @param importDecl An import declaration + * @returns The path of the module to be imported + */ + public 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 { - implementedOrExtendedInterfaces.push(i); + path = importDecl.getModuleSpecifierSourceFile()!.getFilePath(); } - }); - - return implementedOrExtendedInterfaces; -} - -export function processFiles(sourceFiles: Array): void { - sourceFiles.forEach(file => { - logger.info(`File: >>>>>>>>>> ${file.getFilePath()}`); - - if (fs.existsSync(file.getFilePath())) { - currentCC = calculate(file.getFilePath()); - } else { - currentCC = {}; + 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 + */ + public getImplementedOrExtendedInterfaces(interfaces: Array, subClass: ClassDeclaration | InterfaceDeclaration): Array { + let impOrExtInterfaces: Array; + if (subClass instanceof ClassDeclaration) { + impOrExtInterfaces = subClass.getImplements(); } - - 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); + else { + impOrExtInterfaces = subClass.getExtends(); + } + + const interfacesNames = interfaces.map(i => i.getName()); + const implementedOrExtendedInterfaces = new Array(); + + impOrExtInterfaces.forEach(i => { + if (interfacesNames.includes(i.getExpression().getText())) { + implementedOrExtendedInterfaces.push(interfaces[interfacesNames.indexOf(i.getExpression().getText())]); + } + else { + implementedOrExtendedInterfaces.push(i); + } + }); + + return implementedOrExtendedInterfaces; } - - 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); + + public processFiles(sourceFiles: Array): void { + sourceFiles.forEach(file => { + logger.info(`File: >>>>>>>>>> ${file.getFilePath()}`); + + if (fs.existsSync(file.getFilePath())) { + this.currentCC = calculate(file.getFilePath()); + } else { + this.currentCC = {}; + } + + this.processFile(file); + }); + } + + /** + * Builds a Famix model for a source file + * @param f A source file + */ + private processFile(f: SourceFile): void { + const isModule = isSourceFileAModule(f); + + if (isModule) { + this.modules.push(f); } - node.getChildren().forEach(findClasses); + + const exportMap = f.getExportedDeclarations(); + if (exportMap) this.listOfExportMaps.push(exportMap); + + const fmxFile = this.entityDictionary.createOrGetFamixFile(f, isModule); + + logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`); + + this.processComments(f, fmxFile); + this.processAliases(f, fmxFile); + this.processClasses(f, fmxFile); + this.processInterfaces(f, fmxFile); + this.processModules(f, fmxFile); + this.processVariables(f, fmxFile); // This will handle our object literal methods + this.processEnums(f, fmxFile); + this.processFunctions(f, fmxFile); } - - 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); + + /** + * Builds a Famix model for a module (also namespace) + * @param m A namespace + * @returns A Famix.Module representing the module + */ + private processModule(m: ModuleDeclaration): Famix.Module { + const fmxModule = this.entityDictionary.createOrGetFamixModule(m); + + logger.debug(`module: ${m.getName()}, (${m.getType().getText()}), ${fmxModule.fullyQualifiedName}`); + + this.processComments(m, fmxModule); + + this.processAliases(m, fmxModule); + + this.processClasses(m, fmxModule); + + this.processInterfaces(m, fmxModule); + + this.processVariables(m, fmxModule); + + this.processEnums(m, fmxModule); + + this.processFunctions(m, fmxModule); + + this.processModules(m, fmxModule); + + return fmxModule; + } + + /** + * 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 + */ + private processAliases(m: ContainerTypes, fmxScope: ScopedTypes): void { + logger.debug(`processAliases: ---------- Finding Aliases:`); + m.getTypeAliases().forEach(a => { + const fmxAlias = this.processAlias(a); + fmxScope.addAlias(fmxAlias); }); - - // 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 classes of a container + * @param m A container (a source file or a namespace) + * @param fmxScope The Famix model of the container + */ + private processClasses(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void { + logger.debug(`processClasses: ---------- Finding Classes:`); + const classesInArrowFunctions = this.getClassesDeclaredInArrowFunctions(m); + const classes = m.getClasses().concat(classesInArrowFunctions); + classes.forEach(c => { + const fmxClass = this.processClass(c); + fmxScope.addType(fmxClass); + }); + } + + private 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 + */ + private getClassesDeclaredInArrowFunctions(s: SourceFile | ModuleDeclaration): ClassDeclaration[] { + const arrowFunctions = s.getDescendantsOfKind(SyntaxKind.ArrowFunction); + const classesInArrowFunctions = arrowFunctions.map(f => this.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 + */ + private processInterfaces(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void { + logger.debug(`processInterfaces: ---------- Finding Interfaces:`); + m.getInterfaces().forEach(i => { + const fmxInterface = this.processInterface(i); + fmxScope.addType(fmxInterface); }); - }); -} - -/** - * 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); + + /** + * 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 + */ + private 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 = this.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()}`); + this.entityDictionary.createOrGetFamixMethod(method, this.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 + */ + private processEnums(m: ContainerTypes, fmxScope: ScopedTypes): void { + logger.debug(`processEnums: ---------- Finding Enums:`); + m.getEnums().forEach(e => { + const fmxEnum = this.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 + */ + private processFunctions(m: ContainerTypes, fmxScope: ScopedTypes): void { + logger.debug(`Finding Functions:`); + m.getFunctions().forEach(f => { + const fmxFunction = this.processFunction(f); + fmxScope.addFunction(fmxFunction); + }); + + //find arrow functions + logger.debug(`Finding Functions:`); + const arrowFunctions = m.getDescendantsOfKind(SyntaxKind.ArrowFunction); + arrowFunctions.forEach(af => { + const fmxFunction = this.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 + */ + private processModules(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void { + logger.debug(`Finding Modules:`); + m.getModules().forEach(md => { + const fmxModule = this.processModule(md); + fmxScope.addModule(fmxModule); + }); + } + + /** + * Builds a Famix model for an alias + * @param a An alias + * @returns A Famix.Alias representing the alias + */ + private processAlias(a: TypeAliasDeclaration): Famix.Alias { + const fmxAlias = this.entityDictionary.createFamixAlias(a); + + logger.debug(`Alias: ${a.getName()}, (${a.getType().getText()}), fqn = ${fmxAlias.fullyQualifiedName}`); + + this.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 + */ + private processClass(c: ClassDeclaration): Famix.Class | Famix.ParametricClass { + this.classes.push(c); + + const fmxClass = this.entityDictionary.createOrGetFamixClass(c); + + logger.debug(`Class: ${c.getName()}, (${c.getType().getText()}), fqn = ${fmxClass.fullyQualifiedName}`); + + this.processComments(c, fmxClass); + + this.processDecorators(c, fmxClass); + + this.processStructuredType(c, fmxClass); + + c.getConstructors().forEach(con => { + const fmxCon = this.processMethod(con); + fmxClass.addMethod(fmxCon); + }); + + c.getGetAccessors().forEach(acc => { + const fmxAcc = this.processMethod(acc); + fmxClass.addMethod(fmxAcc); + }); + + c.getSetAccessors().forEach(acc => { + const fmxAcc = this.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 + */ + private processInterface(i: InterfaceDeclaration): Famix.Interface | Famix.ParametricInterface { + this.interfaces.push(i); + + const fmxInterface = this.entityDictionary.createOrGetFamixInterface(i); + + logger.debug(`Interface: ${i.getName()}, (${i.getType().getText()}), fqn = ${fmxInterface.fullyQualifiedName}`); + + this.processComments(i, fmxInterface); + + this.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 + */ + private 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) { + this.processTypeParameters(c, fmxScope); } + + c.getProperties().forEach(prop => { + const fmxProperty = this.processProperty(prop); + fmxScope.addProperty(fmxProperty); + }); + + c.getMethods().forEach(m => { + const fmxMethod = this.processMethod(m); + fmxScope.addMethod(fmxMethod); + }); } - - 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 + + /** + * Builds a Famix model for a property + * @param p A property + * @returns A Famix.Property representing the property */ -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); + private processProperty(p: PropertyDeclaration | PropertySignature): Famix.Property { + const fmxProperty = this.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)) { + this.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}`); + this.accessMap.set(fmxProperty.id, p); + } + } + + this.processComments(p, fmxProperty); + + return fmxProperty; } - - if (m instanceof MethodDeclaration || m instanceof GetAccessorDeclaration || m instanceof SetAccessorDeclaration) { - processDecorators(m, fmxMethod); + + /** + * 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 + */ + private processMethod(m: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration): Famix.Method | Famix.Accessor { + const fmxMethod = this.entityDictionary.createOrGetFamixMethod(m, this.currentCC); + + logger.debug(`Method: ${!(m instanceof ConstructorDeclaration) ? m.getName() : "constructor"}, (${m.getType().getText()}), parent: ${(m.getParent() as ClassDeclaration | InterfaceDeclaration).getName()}, fqn = ${fmxMethod.fullyQualifiedName}`); + + this.processComments(m, fmxMethod); + + this.processTypeParameters(m, fmxMethod); + + this.processParameters(m, fmxMethod); + + if (!(m instanceof MethodSignature)) { + this.processAliases(m, fmxMethod); + + this.processVariables(m, fmxMethod); + + this.processEnums(m, fmxMethod); + + this.processFunctions(m, fmxMethod); + + this.processFunctionExpressions(m, fmxMethod); + + this.methodsAndFunctionsWithId.set(fmxMethod.id, m); + } + + if (m instanceof MethodDeclaration || m instanceof GetAccessorDeclaration || m instanceof SetAccessorDeclaration) { + this.processDecorators(m, fmxMethod); + } + + return 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); + + /** + * Builds a Famix model for a function + * @param f A function + * @returns A Famix.Function representing the function + */ + private 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, this.entityDictionary.getAbsolutePath())}`); + + let fmxFunction; + if (f instanceof ArrowFunction) { + fmxFunction = this.entityDictionary.createOrGetFamixArrowFunction(f, this.currentCC); + } else { + fmxFunction = this.entityDictionary.createOrGetFamixFunction(f, this.currentCC); + } + + this.processComments(f, fmxFunction); + + this.processAliases(f, fmxFunction); + + this.processTypeParameters(f, fmxFunction); + + this.processParameters(f, fmxFunction); + + this.processVariables(f, fmxFunction); + + this.processEnums(f, fmxFunction); + + this.processFunctions(f, fmxFunction); + + if (f instanceof FunctionDeclaration && !(f.getParent() instanceof Block)) { + this.processFunctionExpressions(f, fmxFunction); + } + + this.methodsAndFunctionsWithId.set(fmxFunction.id, f); + + return fmxFunction; } - - 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); + + /** + * 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 + */ + private 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 = this.processFunction(func); + fmxScope.addFunction(fmxFunc); + }); } - - 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); + + /** + * 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 + */ + private 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 = this.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 = this.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 + private processParameterAsProperty(param: ParameterDeclaration, classDecl: ClassDeclaration | ClassExpression): Famix.Property { + // Convert the parameter into a Property + const propertyRepresentation = this.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()}`); } - - }); -} - -// 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 = this.entityDictionary.createFamixProperty(property); + if (classDecl instanceof ClassDeclaration) { + const fmxClass = this.entityDictionary.createOrGetFamixClass(classDecl); + fmxClass.addProperty(fmxProperty); + } else { + throw new Error("Unexpected type ClassExpression."); + } + + this.processComments(property, fmxProperty); + + // remove the property from the class + property.remove(); + + return fmxProperty; + } - const fmxProperty = entityDictionary.createFamixProperty(property); - if (classDecl instanceof ClassDeclaration) { - const fmxClass = entityDictionary.createOrGetFamixClass(classDecl); - fmxClass.addProperty(fmxProperty); - } else { - throw new Error("Unexpected type ClassExpression."); + + private 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; } - - 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.`); + + /** + * Builds a Famix model for a parameter + * @param paramDecl A parameter + * @returns A Famix.Parameter representing the parameter + */ + private processParameter(paramDecl: ParameterDeclaration): Famix.Parameter { + const fmxParam = this.entityDictionary.createOrGetFamixParameter(paramDecl); // create or GET + + logger.debug(`parameter: ${paramDecl.getName()}, (${paramDecl.getType().getText()}), fqn = ${fmxParam.fullyQualifiedName}`); + + this.processComments(paramDecl, fmxParam); + + this.processDecorators(paramDecl, fmxParam); + + const parent = paramDecl.getParent(); + + if (!(parent instanceof MethodSignature)) { + logger.debug(`adding access: ${paramDecl.getName()}, (${paramDecl.getType().getText()}) Famix ${fmxParam.name}`); + this.accessMap.set(fmxParam.id, paramDecl); + } + + return fmxParam; } - - // 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); + + private 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 (this.processedNodesWithTypeParams.has(nodeStart)) { + return; + } + + // Get type parameters + const typeParams = e.getTypeParameters(); + + // Process each type parameter + typeParams.forEach((tp) => { + const fmxParam = this.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 + this.processedNodesWithTypeParams.add(nodeStart); } - - 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; + + /** + * Builds a Famix model for a type parameter + * @param tp A type parameter + * @returns A Famix.TypeParameter representing the type parameter + */ + private processTypeParameter(tp: TypeParameterDeclaration): Famix.ParameterType { + const fmxTypeParameter = this.entityDictionary.createFamixParameterType(tp); + logger.debug(`type parameter: ${tp.getName()}, (${tp.getType().getText()}), fqn = ${fmxTypeParameter.fullyQualifiedName}`); + this.processComments(tp, fmxTypeParameter); + return fmxTypeParameter; } - - // 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`); + + /** + * 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 + */ + private processVariableStatement(v: VariableStatement): Array { + const fmxVariables = new Array(); + + logger.debug(`Variable statement: ${v.getText()}, (${v.getType().getText()}), ${v.getDeclarationKindKeywords()[0]}, fqn = ${v.getDeclarations()[0].getName()}`); + + v.getDeclarations().forEach(variable => { + const fmxVar = this.processVariable(variable); + this.processComments(v, fmxVar); + fmxVariables.push(fmxVar); + }); + + return fmxVariables; } - - // 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 { - const fmxVariables = new Array(); - - 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): void { - logger.debug(`Creating accesses:`); - accessMap.forEach((v, id) => { - logger.debug(`Accesses to ${v.getName()}`); + + /** + * Builds a Famix model for a variable + * @param v A variable + * @returns A Famix.Variable representing the variable + */ + private processVariable(v: VariableDeclaration): Famix.Variable { + const fmxVar = this.entityDictionary.createOrGetFamixVariable(v); + + logger.debug(`variable: ${v.getName()}, (${v.getType().getText()}), ${v.getInitializer() ? "initializer: " + v.getInitializer()!.getText() : "initializer: "}, fqn = ${fmxVar.fullyQualifiedName}`); + + this.processComments(v, fmxVar); + + logger.debug(`adding access: ${v.getName()}, (${v.getType().getText()}) Famix ${fmxVar.name}`); + this.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 + */ + private processEnum(e: EnumDeclaration): Famix.Enum { + const fmxEnum = this.entityDictionary.createOrGetFamixEnum(e); + + logger.debug(`enum: ${e.getName()}, (${e.getType().getText()}), fqn = ${fmxEnum.fullyQualifiedName}`); + + this.processComments(e, fmxEnum); + + e.getMembers().forEach(m => { + const fmxEnumValue = this.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 + */ + private processEnumValue(v: EnumMember): Famix.EnumValue { + const fmxEnumValue = this.entityDictionary.createFamixEnumValue(v); + + logger.debug(`enum value: ${v.getName()}, (${v.getType().getText()}), fqn = ${fmxEnumValue.fullyQualifiedName}`); + + this.processComments(v, fmxEnumValue); + + logger.debug(`adding access: ${v.getName()}, (${v.getType().getText()}) Famix ${fmxEnumValue.name}`); + this.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 + */ + private 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 = this.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 + */ + private processDecorator(d: Decorator, e: ClassDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ParameterDeclaration | PropertyDeclaration): Famix.Decorator { + const fmxDec = this.entityDictionary.createOrGetFamixDecorator(d, e); + + logger.debug(`decorator: ${d.getName()}, (${d.getType().getText()}), fqn = ${fmxDec.fullyQualifiedName}`); + + this.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 + */ + private 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 = this.processComment(c, fmxScope); + logger.debug(`leading comments, addComment: '${c.getText()}'`); + fmxScope.addComment(fmxComment); // redundant, but just in case + }); + e.getTrailingCommentRanges().forEach(c => { + const fmxComment = this.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 + */ + private processComment(c: CommentRange, fmxScope: Famix.NamedEntity): Famix.Comment { + const isJSDoc = c.getText().startsWith("/**"); + logger.debug(`processComment: comment: ${c.getText()}, isJSDoc = ${isJSDoc}`); + const fmxComment = this.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 + */ + public processAccesses(accessMap: Map): void { + logger.debug(`Creating accesses:`); + accessMap.forEach((v, id) => { + logger.debug(`Accesses to ${v.getName()}`); + // try { + const temp_nodes = v.findReferencesAsNodes() as Array; + temp_nodes.forEach(node => this.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 + */ + private processNodeForAccesses(n: Identifier, id: number): void { // try { - const temp_nodes = v.findReferencesAsNodes() as Array; - temp_nodes.forEach(node => processNodeForAccesses(node, id)); + // 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; + } + this.entityDictionary.createFamixAccess(n, id); + logger.debug(`processNodeForAccesses: node kind: ${n.getKindName()}, ${n.getText()}, (${n.getType().getText()})`); // } catch (error) { - // logger.error(`> WARNING: got exception "${error}".\nContinuing...`); + // logger.error(`> Got exception "${error}".\nScopeDeclaration invalid for "${n.getSymbol().fullyQualifiedName}".\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, exports: Array>): 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 + + + // exports has name -> Declaration -- the declaration can be used to find the FamixElement + + // handle `import path = require("path")` for example + public processImportClausesForImportEqualsDeclarations(sourceFiles: Array, exports: Array>): 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(); + this.entityDictionary.oldCreateOrGetFamixImportClause({ + importDeclaration: node, + importerSourceFile: sourceFile, + moduleSpecifierFilePath: node.getModuleReference().getText(), + importElement: namedImport, + isInExports: exports.find(e => e.has(namedImport.getText())) !== undefined, + isDefaultExport: false + }); + // this.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 + */ + public processImportClausesForModules(modules: Array, exports: Array>): 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 = this.getModulePath(impDecl); + + impDecl.getNamedImports().forEach(namedImport => { + logger.info(`Importing (named) ${namedImport.getName()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); + const importedEntityName = namedImport.getName(); + const importFoundInExports = this.isInExports(exports, importedEntityName); + logger.debug(`Processing ImportSpecifier: ${namedImport.getText()}, pos=${namedImport.getStart()}`); + this.entityDictionary.oldCreateOrGetFamixImportClause({ + importDeclaration: impDecl, + importerSourceFile: module, + moduleSpecifierFilePath: path, + importElement: namedImport, + isInExports: importFoundInExports, + isDefaultExport: false + }); }); - // entityDictionary.createFamixImportClause(importedEntity, importingEntity); + + 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()}`); + this.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}`); + this.entityDictionary.oldCreateOrGetFamixImportClause({ + importDeclaration: impDecl, + importerSourceFile: module, + moduleSpecifierFilePath: path, + importElement: namespaceImport, + isInExports: false, + isDefaultExport: false + }); + } + }); + }); + } + + private isInExports(exports: ReadonlyMap[], importedEntityName: string) { + let importFoundInExports = false; + exports.forEach(e => { + if (e.has(importedEntityName)) { + importFoundInExports = true; } }); + return importFoundInExports; } - ); -} - -/** - * 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, exports: Array>): 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 + + /** + * 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 + */ + public 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) { + this.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) { + this.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 = this.getImplementedOrExtendedInterfaces(interfaces, cls); + implementedInterfaces.forEach(implementedIF => { + this.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()})`); }); - }); - - 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 + }); + + interfaces.forEach(interFace => { + try { + logger.debug(`Checking interface inheritance for ${interFace.getName()}`); + const extendedInterfaces = this.getImplementedOrExtendedInterfaces(interfaces, interFace); + extendedInterfaces.forEach(extendedInterface => { + this.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()})`); }); } - - 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 - }); + catch (error) { + logger.error(`> WARNING: got exception ${error}. Continuing...`); } }); - }); -} - -function isInExports(exports: ReadonlyMap[], 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()}`); + } + + /** + * 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 + */ + public processInvocations(methodsAndFunctionsWithId: Map): 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; + nodesReferencingInvocable.forEach( + nodeReferencingInvocable => this.processNodeForInvocations(nodeReferencingInvocable, invocable, id)); + } catch (error) { + logger.error(`> WARNING: got exception ${error}. Continuing...`); } + } else { + logger.debug(`Skipping invocation to ArrowFunction: ${(invocable.getBodyText())}`); } - - 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 => { + }); + } + + /** + * 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 + */ + private processNodeForInvocations(nodeReferencingInvocable: Identifier, invocable: InvocableType, id: number): void { 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...`); + this.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 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): 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; - nodesReferencingInvocable.forEach( - nodeReferencingInvocable => processNodeForInvocations(nodeReferencingInvocable, invocable, id)); - } catch (error) { - logger.error(`> WARNING: got exception ${error}. 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 + */ + public processConcretisations(classes: ClassDeclaration[], interfaces: InterfaceDeclaration[], functions: Map): void { + logger.info(`processConcretisations: Creating concretisations:`); + classes.forEach(cls => { + logger.debug(`processConcretisations: Checking class concretisation for ${cls.getName()}`); + this.entityDictionary.createFamixConcretisationClassOrInterfaceSpecialisation(cls); + this.entityDictionary.createFamixConcretisationGenericInstantiation(cls); + this.entityDictionary.createFamixConcretisationInterfaceClass(cls); + this.entityDictionary.createFamixConcretisationTypeInstanciation(cls); + + }); + interfaces.forEach(inter => { + logger.debug(`processConcretisations: Checking interface concretisation for ${inter.getName()}`); + this.entityDictionary.createFamixConcretisationTypeInstanciation(inter); + this.entityDictionary.createFamixConcretisationClassOrInterfaceSpecialisation(inter); + }); + functions.forEach(func => { + if(func instanceof FunctionDeclaration || func instanceof MethodDeclaration ){ + logger.debug(`processConcretisations: Checking Method concretisation`); + this.entityDictionary.createFamixConcretisationFunctionInstantiation(func); } - } 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...`); + }); } } + +export function isAmbient(node: ModuleDeclaration): boolean { + // An ambient module has the DeclareKeyword modifier. + return (node.getModifiers()?.some(modifier => modifier.getKind() === SyntaxKind.DeclareKeyword)) ?? false; +} -/** - * 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): 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); - } - }); +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; } diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index e631762f..765f6900 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -9,12 +9,13 @@ import { ClassDeclaration, ConstructorDeclaration, FunctionDeclaration, Identifi import { isAmbient, isNamespace } from "../analyze_functions/process_functions"; import * as Famix from "../lib/famix/model/famix"; import { FamixRepository } from "../lib/famix/famix_repository"; -import { logger, config } from "../analyze"; +import { logger } from "../analyze"; // eslint-disable-next-line @typescript-eslint/no-require-imports import GraphemeSplitter = require('grapheme-splitter'); import * as Helpers from "./helpers_creation"; import * as FQNFunctions from "../fqn"; import path from "path"; +import { convertToRelativePath } from "./helpers_path"; export type TSMorphObjectType = ImportDeclaration | ImportEqualsDeclaration | SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | TypeParameterDeclaration | Identifier | Decorator | GetAccessorDeclaration | SetAccessorDeclaration | ImportSpecifier | CommentRange | EnumDeclaration | EnumMember | TypeAliasDeclaration | ExpressionWithTypeArguments | TSMorphParametricType; @@ -28,8 +29,13 @@ type ConcreteElementTSMorphType = ClassDeclaration | InterfaceDeclaration | Func export type InvocableType = MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ArrowFunction; +export type EntityDictionaryConfig = { + expectGraphemes: boolean; +} + export class EntityDictionary { - + private config: EntityDictionaryConfig; + private absolutePath: string = ""; public famixRep = new FamixRepository(); private fmxAliasMap = new Map(); // Maps the alias names to their Famix model private fmxClassMap = new Map(); // Maps the fully qualified class names to their Famix model @@ -49,8 +55,16 @@ export class EntityDictionary { public fmxElementObjectMap = new Map(); public tsMorphElementObjectMap = new Map(); - constructor() { - this.famixRep.setFmxElementObjectMap(this.fmxElementObjectMap); + constructor(config: EntityDictionaryConfig) { + this.config = config; + } + + public getAbsolutePath(): string { + return this.absolutePath; + } + + public setAbsolutePath(path: string) { + this.absolutePath = path; } public addSourceAnchor(fmx: Famix.SourcedEntity, node: TSMorphObjectType): Famix.IndexedFileAnchor { @@ -66,7 +80,7 @@ export class EntityDictionary { sourceEnd = node.getEnd(); } - if (config.expectGraphemes) { + if (this.config.expectGraphemes) { /** * The following logic handles the case of multi-code point characters (e.g. emoji) in the source text. * This is needed because Pharo/Smalltalk treats multi-code point characters as a single character, @@ -132,7 +146,7 @@ export class EntityDictionary { this.fmxElementObjectMap.set(famixElement, sourceElement); if (sourceElement !== null) { - const absolutePathProject = this.famixRep.getAbsolutePath(); + const absolutePathProject = this.getAbsolutePath(); const absolutePath = path.normalize(sourceElement.getSourceFile().getFilePath()); @@ -144,7 +158,7 @@ export class EntityDictionary { const pathFromNodeModules = absolutePath.substring(positionNodeModules); pathInProject = pathFromNodeModules; } else { - pathInProject = this.convertToRelativePath(absolutePath, absolutePathProject); + pathInProject = convertToRelativePath(absolutePath, absolutePathProject); } // revert any backslashes to forward slashes (path.normalize on windows introduces them) @@ -167,7 +181,7 @@ export class EntityDictionary { sourceStart = sourceElement.getPos(); sourceEnd = sourceElement.getEnd(); } - if (config.expectGraphemes) { + if (this.config.expectGraphemes) { /** * The following logic handles the case of multi-code point characters (e.g. emoji) in the source text. * This is needed because Pharo/Smalltalk treats multi-code point characters as a single character, @@ -235,7 +249,7 @@ export class EntityDictionary { fmxFile.numberOfLinesOfText = f.getEndLineNumber() - f.getStartLineNumber(); fmxFile.numberOfCharacters = f.getFullText().length; - initFQN(f, fmxFile); + this.initFQN(f, fmxFile); this.makeFamixIndexFileAnchor(f, fmxFile); @@ -272,7 +286,7 @@ export class EntityDictionary { fmxModule.isNamespace = isNamespace(moduleDeclaration); fmxModule.isModule = !fmxModule.isNamespace && !fmxModule.isAmbient; - initFQN(moduleDeclaration, fmxModule); + this.initFQN(moduleDeclaration, fmxModule); this.makeFamixIndexFileAnchor(moduleDeclaration, fmxModule); this.fmxModuleMap.set(moduleDeclaration, fmxModule); @@ -292,7 +306,7 @@ export class EntityDictionary { let fmxAlias: Famix.Alias; const aliasName = typeAliasDeclaration.getName(); //const aliasFullyQualifiedName = a.getType().getText(); // FQNFunctions.getFQN(a); - const aliasFullyQualifiedName = FQNFunctions.getFQN(typeAliasDeclaration); + const aliasFullyQualifiedName = FQNFunctions.getFQN(typeAliasDeclaration, this.getAbsolutePath()); const foundAlias = this.fmxAliasMap.get(aliasFullyQualifiedName); if (!foundAlias) { fmxAlias = new Famix.Alias(); @@ -302,7 +316,7 @@ export class EntityDictionary { const fmxType = this.createOrGetFamixType(aliasNameWithGenerics, typeAliasDeclaration.getType(), typeAliasDeclaration); fmxAlias.aliasedEntity = fmxType; - initFQN(typeAliasDeclaration, fmxAlias); + this.initFQN(typeAliasDeclaration, fmxAlias); this.makeFamixIndexFileAnchor(typeAliasDeclaration, fmxAlias); this.fmxAliasMap.set(aliasFullyQualifiedName, fmxAlias); @@ -325,7 +339,7 @@ export class EntityDictionary { public createOrGetFamixClass(cls: ClassDeclaration): Famix.Class | Famix.ParametricClass { let fmxClass: Famix.Class | Famix.ParametricClass; const isAbstract = cls.isAbstract(); - const classFullyQualifiedName = FQNFunctions.getFQN(cls); + const classFullyQualifiedName = FQNFunctions.getFQN(cls, this.getAbsolutePath()); const clsName = cls.getName() || this.UNKNOWN_VALUE; const isGeneric = cls.getTypeParameters().length; const foundClass = this.fmxClassMap.get(classFullyQualifiedName); @@ -338,7 +352,7 @@ export class EntityDictionary { } fmxClass.name = clsName; - initFQN(cls, fmxClass); + this.initFQN(cls, fmxClass); // fmxClass.fullyQualifiedName = classFullyQualifiedName; fmxClass.isAbstract = isAbstract; @@ -366,7 +380,7 @@ export class EntityDictionary { let fmxInterface: Famix.Interface | Famix.ParametricInterface; const interName = inter.getName(); - const interFullyQualifiedName = FQNFunctions.getFQN(inter); + const interFullyQualifiedName = FQNFunctions.getFQN(inter, this.getAbsolutePath()); const foundInterface = this.fmxInterfaceMap.get(interFullyQualifiedName); if (!foundInterface) { const isGeneric = inter.getTypeParameters().length; @@ -378,7 +392,7 @@ export class EntityDictionary { } fmxInterface.name = interName; - initFQN(inter, fmxInterface); + this.initFQN(inter, fmxInterface); this.makeFamixIndexFileAnchor(inter, fmxInterface); this.fmxInterfaceMap.set(interFullyQualifiedName, fmxInterface); @@ -513,7 +527,7 @@ export class EntityDictionary { fmxProperty.isJavaScriptPrivate = true; } - initFQN(property, fmxProperty); + this.initFQN(property, fmxProperty); this.makeFamixIndexFileAnchor(property, fmxProperty); this.famixRep.addElement(fmxProperty); @@ -536,7 +550,7 @@ export class EntityDictionary { // console.log(`\n=== Creating/Getting Method ===`); // console.log(`Method kind: ${method.getKindName()}`); // console.log(`Method text: ${method.getText().slice(0, 50)}...`); - const fqn = FQNFunctions.getFQN(method); + const fqn = FQNFunctions.getFQN(method, this.getAbsolutePath()); // console.log(`Method FQN: ${fqn}`); logger.debug(`Processing method ${fqn}`); @@ -598,7 +612,7 @@ export class EntityDictionary { fmxMethod.numberOfStatements = isSignature ? 0 : method.getStatements().length; // Add to famixRep - initFQN(method, fmxMethod); + this.initFQN(method, fmxMethod); this.famixRep.addElement(fmxMethod); this.makeFamixIndexFileAnchor(method, fmxMethod); this.fmxFunctionAndMethodMap.set(fqn, fmxMethod); @@ -620,7 +634,7 @@ export class EntityDictionary { public createOrGetFamixFunction(func: FunctionDeclaration | FunctionExpression, currentCC: { [key: string]: number }): Famix.Function | Famix.ParametricFunction { let fmxFunction: Famix.Function | Famix.ParametricFunction; const isGeneric = func.getTypeParameters().length > 0; - const functionFullyQualifiedName = FQNFunctions.getFQN(func); + const functionFullyQualifiedName = FQNFunctions.getFQN(func, this.getAbsolutePath()); if (!this.fmxFunctionAndMethodMap.has(functionFullyQualifiedName)) { if (isGeneric) { fmxFunction = new Famix.ParametricFunction(); @@ -639,7 +653,7 @@ export class EntityDictionary { fmxFunction.signature = Helpers.computeSignature(func.getText()); fmxFunction.cyclomaticComplexity = currentCC[fmxFunction.name]; - initFQN(func, fmxFunction); + this.initFQN(func, fmxFunction); // fmxFunction.fullyQualifiedName = functionFullyQualifiedName; let functionTypeName = this.UNKNOWN_VALUE; @@ -698,7 +712,7 @@ export class EntityDictionary { fmxParam.declaredType = fmxType; fmxParam.name = param.getName(); - initFQN(param, fmxParam); + this.initFQN(param, fmxParam); this.makeFamixIndexFileAnchor(param, fmxParam); this.famixRep.addElement(fmxParam); @@ -719,7 +733,7 @@ export class EntityDictionary { const fmxParameterType = new Famix.ParameterType(); fmxParameterType.name = tp.getName(); - initFQN(tp, fmxParameterType); + this.initFQN(tp, fmxParameterType); this.makeFamixIndexFileAnchor(tp, fmxParameterType); this.famixRep.addElement(fmxParameterType); @@ -845,7 +859,7 @@ export class EntityDictionary { const fmxType = this.createOrGetFamixType(variableTypeName, variable.getType(), variable); fmxVariable.declaredType = fmxType; fmxVariable.name = variable.getName(); - initFQN(variable, fmxVariable); + this.initFQN(variable, fmxVariable); this.makeFamixIndexFileAnchor(variable, fmxVariable); this.famixRep.addElement(fmxVariable); @@ -872,7 +886,7 @@ export class EntityDictionary { } const fmxEnum = new Famix.Enum(); fmxEnum.name = enumEntity.getName(); - initFQN(enumEntity, fmxEnum); + this.initFQN(enumEntity, fmxEnum); this.makeFamixIndexFileAnchor(enumEntity, fmxEnum); this.famixRep.addElement(fmxEnum); @@ -901,7 +915,7 @@ export class EntityDictionary { const fmxType = this.createOrGetFamixType(enumValueTypeName, enumMember.getType(), enumMember); fmxEnumValue.declaredType = fmxType; fmxEnumValue.name = enumMember.getName(); - initFQN(enumMember, fmxEnumValue); + this.initFQN(enumMember, fmxEnumValue); this.makeFamixIndexFileAnchor(enumMember, fmxEnumValue); this.famixRep.addElement(fmxEnumValue); @@ -924,10 +938,10 @@ export class EntityDictionary { fmxDecorator.name = decoratorName; fmxDecorator.decoratorExpression = decoratorExpression; - const decoratedEntityFullyQualifiedName = FQNFunctions.getFQN(decoratedEntity); + const decoratedEntityFullyQualifiedName = FQNFunctions.getFQN(decoratedEntity, this.getAbsolutePath()); const fmxDecoratedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(decoratedEntityFullyQualifiedName) as Famix.NamedEntity; fmxDecorator.decoratedEntity = fmxDecoratedEntity; - initFQN(decorator, fmxDecorator); + this.initFQN(decorator, fmxDecorator); this.makeFamixIndexFileAnchor(decorator, fmxDecorator); this.famixRep.addElement(fmxDecorator); @@ -977,7 +991,7 @@ export class EntityDictionary { } if (element.isKind(SyntaxKind.MethodSignature) || element.isKind(SyntaxKind.MethodDeclaration)) { - const methodFQN = FQNFunctions.getFQN(element); + const methodFQN = FQNFunctions.getFQN(element, this.getAbsolutePath()); const returnTypeFQN = `${methodFQN.replace(/\[Method(Signature|Declaration)\]$/, '')}[ReturnType]`; // Check if we already have this return type in the repository @@ -995,7 +1009,7 @@ export class EntityDictionary { // Set container (same as method's container) const methodAncestor = Helpers.findTypeAncestor(element); if (methodAncestor) { - const ancestorFQN = FQNFunctions.getFQN(methodAncestor); + const ancestorFQN = FQNFunctions.getFQN(methodAncestor, this.getAbsolutePath()); const ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFQN) as Famix.ContainerEntity; if (ancestor) { fmxType.container = ancestor; @@ -1024,7 +1038,7 @@ export class EntityDictionary { // console.log(`Type ancestor found: ${typeAncestor?.getKindName()}`); if (typeAncestor) { - const ancestorFullyQualifiedName = FQNFunctions.getFQN(typeAncestor); + const ancestorFullyQualifiedName = FQNFunctions.getFQN(typeAncestor, this.getAbsolutePath()); // console.log(`Ancestor FQN: ${ancestorFullyQualifiedName}`); ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity; if (!ancestor) { @@ -1047,7 +1061,7 @@ export class EntityDictionary { throw new Error(`Ancestor not found for type ${typeName}.`); } - initFQN(element, fmxType); + this.initFQN(element, fmxType); // console.log(`Type FQN after init: ${fmxType.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(element, fmxType); this.famixRep.addElement(fmxType); @@ -1126,7 +1140,7 @@ export class EntityDictionary { // (fmxType as Famix.ParameterType).baseType = fmxBaseType; fmxType.name = typeName; - initFQN(element, fmxType); + this.initFQN(element, fmxType); this.famixRep.addElement(fmxType); this.fmxTypeMap.set(element, fmxType); return fmxType; @@ -1199,7 +1213,7 @@ export class EntityDictionary { return; } - const ancestorFullyQualifiedName = FQNFunctions.getFQN(nodeReferenceAncestor); + const ancestorFullyQualifiedName = FQNFunctions.getFQN(nodeReferenceAncestor, this.getAbsolutePath()); const accessor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity; if (!accessor) { logger.error(`Ancestor ${ancestorFullyQualifiedName} of kind ${nodeReferenceAncestor.getKindName()} not found.`); @@ -1240,7 +1254,7 @@ export class EntityDictionary { // since the node is in the AST, we need to find the ancestor that is in the Famix model const containerOfNode = Helpers.findAncestor(nodeReferringToInvocable); logger.debug(`Found container (ancestor) ${containerOfNode.getKindName()} for AST node ${nodeReferringToInvocable.getText()}.`); - const containerFQN = FQNFunctions.getFQN(containerOfNode); + const containerFQN = FQNFunctions.getFQN(containerOfNode, this.getAbsolutePath()); logger.debug(`Found containerFQN ${containerFQN}.`); let sender = this.famixRep.getFamixEntityByFullyQualifiedName(containerFQN) as Famix.ContainerEntity; logger.debug(`Found a sender that matches ${sender.fullyQualifiedName}.`); @@ -1252,7 +1266,7 @@ export class EntityDictionary { sender = senderContainer; } } - const receiverFullyQualifiedName = FQNFunctions.getFQN(invocable.getParent()); + const receiverFullyQualifiedName = FQNFunctions.getFQN(invocable.getParent(), this.getAbsolutePath()); const receiver = this.famixRep.getFamixEntityByFullyQualifiedName(receiverFullyQualifiedName) as Famix.NamedEntity; const fmxInvocation = new Famix.Invocation(); @@ -1420,12 +1434,12 @@ export class EntityDictionary { let importedEntity: Famix.NamedEntity | Famix.StructuralEntity | undefined = undefined; let importedEntityName: string; - const absolutePathProject = this.famixRep.getAbsolutePath(); + const absolutePathProject = this.getAbsolutePath(); const absolutePath = path.normalize(moduleSpecifierFilePath); logger.debug(`createFamixImportClause: absolutePath: ${absolutePath}`); - logger.debug(`createFamixImportClause: convertToRelativePath: ${this.convertToRelativePath(absolutePath, absolutePathProject)}`); - const pathInProject: string = this.convertToRelativePath(absolutePath, absolutePathProject).replace(/\\/g, "/"); + logger.debug(`createFamixImportClause: convertToRelativePath: ${convertToRelativePath(absolutePath, absolutePathProject)}`); + const pathInProject: string = convertToRelativePath(absolutePath, absolutePathProject).replace(/\\/g, "/"); logger.debug(`createFamixImportClause: pathInProject: ${pathInProject}`); let pathName = "{" + pathInProject + "}."; logger.debug(`createFamixImportClause: pathName: ${pathName}`); @@ -1445,7 +1459,7 @@ export class EntityDictionary { importedEntity.isStub = true; } logger.debug(`Creating named entity ${importedEntityName} for ImportSpecifier ${importElement.getText()}`); - initFQN(importElement, importedEntity); + this.initFQN(importElement, importedEntity); logger.debug(`Assigned FQN to entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); this.famixRep.addElement(importedEntity); @@ -1457,7 +1471,7 @@ export class EntityDictionary { pathName = pathName + importedEntityName; importedEntity = new Famix.StructuralEntity(); importedEntity.name = importedEntityName; - initFQN(importDeclaration, importedEntity); + this.initFQN(importDeclaration, importedEntity); logger.debug(`Assigned FQN to ImportEquals entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); const anyType = this.createOrGetFamixType('any', undefined, importDeclaration); @@ -1467,7 +1481,7 @@ export class EntityDictionary { pathName = pathName + (isDefaultExport ? "defaultExport" : "namespaceExport"); importedEntity = new Famix.NamedEntity(); importedEntity.name = importedEntityName; - initFQN(importElement, importedEntity); + this.initFQN(importElement, importedEntity); logger.debug(`Assigned FQN to default/namespace entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); } @@ -1475,7 +1489,7 @@ export class EntityDictionary { this.famixRep.addElement(importedEntity); logger.debug(`Added non-exported entity to repository: ${importedEntity.fullyQualifiedName}`); } - const importerFullyQualifiedName = FQNFunctions.getFQN(importer); + const importerFullyQualifiedName = FQNFunctions.getFQN(importer, this.getAbsolutePath()); const fmxImporter = this.famixRep.getFamixEntityByFullyQualifiedName(importerFullyQualifiedName) as Famix.Module; fmxImportClause.importingEntity = fmxImporter; fmxImportClause.importedEntity = importedEntity; @@ -1504,7 +1518,7 @@ export class EntityDictionary { public createOrGetFamixArrowFunction(arrowExpression: Expression, currentCC: { [key: string]: number } ): Famix.ArrowFunction | Famix.ParametricArrowFunction { let fmxArrowFunction: Famix.ArrowFunction | Famix.ParametricArrowFunction; - const functionFullyQualifiedName = FQNFunctions.getFQN(arrowExpression); + const functionFullyQualifiedName = FQNFunctions.getFQN(arrowExpression, this.getAbsolutePath()); if (!this.fmxFunctionAndMethodMap.has(functionFullyQualifiedName)) { @@ -1554,7 +1568,7 @@ export class EntityDictionary { const parameters = arrowFunction.getParameters(); fmxArrowFunction.numberOfParameters = parameters.length; fmxArrowFunction.numberOfStatements = arrowFunction.getStatements().length; - initFQN(arrowExpression as unknown as TSMorphObjectType, fmxArrowFunction); + this.initFQN(arrowExpression as unknown as TSMorphObjectType, fmxArrowFunction); this.makeFamixIndexFileAnchor(arrowExpression as unknown as TSMorphObjectType, fmxArrowFunction); this.famixRep.addElement(fmxArrowFunction); this.fmxElementObjectMap.set(fmxArrowFunction,arrowFunction as unknown as TSMorphObjectType); @@ -1868,15 +1882,22 @@ export class EntityDictionary { } } - public convertToRelativePath(absolutePath: string, absolutePathProject: string) { - logger.debug(`convertToRelativePath: absolutePath: '${absolutePath}', absolutePathProject: '${absolutePathProject}'`); - if (absolutePath.startsWith(absolutePathProject)) { - return absolutePath.replace(absolutePathProject, "").slice(1); - } else if (absolutePath.startsWith("/")) { - return absolutePath.slice(1); - } else { - return absolutePath; + private initFQN(sourceElement: TSMorphObjectType, famixElement: Famix.SourcedEntity) { + // handle special cases where an element is a Type -- need to change its name + if (famixElement instanceof Famix.Type && !(sourceElement instanceof CommentRange) && isTypeContext(sourceElement)) { + let fqn = FQNFunctions.getFQN(sourceElement, this.getAbsolutePath()); + // using regex, replace [blah] with [blahType] + fqn = fqn.replace(/\[([^\]]+)\]/g, "[$1Type]"); + logger.debug("Setting fully qualified name for " + famixElement.getJSON() + " to " + fqn); + famixElement.fullyQualifiedName = fqn; + return; } + // catch all (except comments) + if (!(sourceElement instanceof CommentRange)) { + const fqn = FQNFunctions.getFQN(sourceElement, this.getAbsolutePath()); + logger.debug("Setting fully qualified name for " + famixElement.getJSON() + " to " + fqn); + (famixElement as Famix.NamedEntity).fullyQualifiedName = fqn; + } } } @@ -1895,25 +1916,6 @@ export function isPrimitiveType(typeName: string) { typeName === "void"; } -function initFQN(sourceElement: TSMorphObjectType, famixElement: Famix.SourcedEntity) { - // handle special cases where an element is a Type -- need to change its name - if (famixElement instanceof Famix.Type && !(sourceElement instanceof CommentRange) && isTypeContext(sourceElement)) { - let fqn = FQNFunctions.getFQN(sourceElement); - // using regex, replace [blah] with [blahType] - fqn = fqn.replace(/\[([^\]]+)\]/g, "[$1Type]"); - logger.debug("Setting fully qualified name for " + famixElement.getJSON() + " to " + fqn); - famixElement.fullyQualifiedName = fqn; - return; - } - // catch all (except comments) - if (!(sourceElement instanceof CommentRange)) { - const fqn = FQNFunctions.getFQN(sourceElement); - logger.debug("Setting fully qualified name for " + famixElement.getJSON() + " to " + fqn); - (famixElement as Famix.NamedEntity).fullyQualifiedName = fqn; - } -} - - function isTypeContext(sourceElement: TSMorphObjectType): boolean { // Just keep the existing SyntaxKind set as it is const typeContextKinds = new Set([ diff --git a/src/famix_functions/helpers_path.ts b/src/famix_functions/helpers_path.ts new file mode 100644 index 00000000..b3811b59 --- /dev/null +++ b/src/famix_functions/helpers_path.ts @@ -0,0 +1,12 @@ +import { logger } from "../analyze"; + +export function convertToRelativePath(absolutePath: string, absolutePathProject: string) { + logger.debug(`convertToRelativePath: absolutePath: '${absolutePath}', absolutePathProject: '${absolutePathProject}'`); + if (absolutePath.startsWith(absolutePathProject)) { + return absolutePath.replace(absolutePathProject, "").slice(1); + } else if (absolutePath.startsWith("/")) { + return absolutePath.slice(1); + } else { + return absolutePath; + } +} \ No newline at end of file diff --git a/src/fqn.ts b/src/fqn.ts index 73b71290..59472f7e 100644 --- a/src/fqn.ts +++ b/src/fqn.ts @@ -1,609 +1,608 @@ -import { ArrowFunction, CallExpression, ClassDeclaration, ConstructorDeclaration, Decorator, EnumDeclaration, ExpressionWithTypeArguments, FunctionDeclaration, FunctionExpression, GetAccessorDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, Node, PropertyDeclaration, SetAccessorDeclaration, SourceFile, SyntaxKind, TypeParameterDeclaration, VariableDeclaration } from "ts-morph"; -import { entityDictionary, logger } from "./analyze"; -import path from "path"; -import { TSMorphTypeDeclaration } from "./famix_functions/EntityDictionary"; - -type FQNNode = SourceFile | VariableDeclaration | ArrowFunction | Identifier | MethodDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | PropertyDeclaration | TSMorphTypeDeclaration | EnumDeclaration | ImportDeclaration | ImportEqualsDeclaration | CallExpression | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | Decorator | ModuleDeclaration; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function isFQNNode(node: Node): node is FQNNode { - return Node.isVariableDeclaration(node) || Node.isArrowFunction(node) || Node.isIdentifier(node) || Node.isMethodDeclaration(node) || Node.isClassDeclaration(node) || Node.isClassExpression(node) || Node.isDecorator(node) || Node.isModuleDeclaration(node) || Node.isCallExpression(node); -} - -/** - * Builds a map of method positions to their property keys in object literals. - * Scans all variable declarations in a source file, targeting object literals with any keys - * (e.g., `3: { method() {} }` or `add: { compute() {} }`), and maps each method's start position to its key. - * Logs each step for debugging. - * - * @param sourceFile The TypeScript source file to analyze - * @returns A Map where keys are method start positions and values are their property keys (e.g., "3", "add") - */ -function buildStageMethodMap(sourceFile: SourceFile): Map { - const stageMap = new Map(); - - sourceFile.getVariableDeclarations().forEach(varDecl => { - // const varName = varDecl.getName(); - const initializer = varDecl.getInitializer(); - - if (!initializer || !Node.isObjectLiteralExpression(initializer)) { - return; - } - - initializer.getProperties().forEach(prop => { - let key: string | undefined; - - if (Node.isPropertyAssignment(prop)) { - const nameNode = prop.getNameNode(); - - if (Node.isIdentifier(nameNode)) { - key = nameNode.getText(); - } else if (Node.isStringLiteral(nameNode)) { - key = nameNode.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); - } else if (Node.isNumericLiteral(nameNode)) { - key = nameNode.getText(); - } else if (Node.isComputedPropertyName(nameNode)) { - const expression = nameNode.getExpression(); - if (Node.isIdentifier(expression)) { - // Resolve variable value if possible - const symbol = expression.getSymbol(); - if (symbol) { - const decl = symbol.getDeclarations()[0]; - if (Node.isVariableDeclaration(decl) && decl.getInitializer()) { - const init = decl.getInitializer()!; - if (Node.isStringLiteral(init) || Node.isNumericLiteral(init)) { - key = init.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); - } - } - } - if (!key) { - key = expression.getText(); - } - } else if (Node.isBinaryExpression(expression) && expression.getOperatorToken().getText() === '+') { - // Handle simple string concatenation (e.g., "A" + "B") - const left = expression.getLeft(); - const right = expression.getRight(); - if (Node.isStringLiteral(left) && Node.isStringLiteral(right)) { - key = left.getLiteralText() + right.getLiteralText(); - } - } else if (Node.isTemplateExpression(expression)) { - // Handle template literals (e.g., `key-${1}`) - const head = expression.getHead().getLiteralText(); - const spans = expression.getTemplateSpans(); - if (spans.length === 1 && Node.isNumericLiteral(spans[0].getExpression())) { - const num = spans[0].getExpression().getText(); - key = `${head}${num}`; - } - } - if (!key) { - key = expression.getText(); // Fallback - } - } else { - return; - } - - const propInitializer = prop.getInitializer(); - if (propInitializer && Node.isObjectLiteralExpression(propInitializer)) { - propInitializer.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => { - // const methodName = method.getName(); - const pos = method.getStart(); - if (key) { - stageMap.set(pos, key); - } - }); - } - } - }); - }); - - return stageMap; -} - -/** - * Builds a map of method positions to their index in class/interface/namespace declarations - * @param sourceFile The TypeScript source file to analyze - * @returns A Map where keys are method start positions and values are their positional index (1-based) - */ -function buildMethodPositionMap(sourceFile: SourceFile): Map { - const positionMap = new Map(); - // console.log(`[buildMethodPositionMap] Starting analysis for file: ${sourceFile.getFilePath()}`); - - // Helper function to process modules recursively - function processModule(moduleNode: ModuleDeclaration, modulePath: string) { - // console.log(`[buildMethodPositionMap] Processing module: ${modulePath}`); - - // Handle functions directly in the module - const functions = moduleNode.getFunctions(); - const functionCounts = new Map(); - - functions.forEach(func => { - const funcName = func.getName() || `Unnamed_Function(${func.getStart()})`; - const count = (functionCounts.get(funcName) || 0) + 1; - functionCounts.set(funcName, count); - positionMap.set(func.getStart(), count); - // console.log(`[buildMethodPositionMap] Module function: ${funcName}, position: ${func.getStart()}, index: ${count}`); - }); - - // Handle classes within the module - const classes = moduleNode.getClasses(); - classes.forEach(classNode => { - // console.log(`[buildMethodPositionMap] Processing class in module: ${classNode.getName() || 'Unnamed'}`); - const methods = classNode.getMethods(); - const methodCounts = new Map(); - - methods.forEach(method => { - const methodName = method.getName(); - const count = (methodCounts.get(methodName) || 0) + 1; - methodCounts.set(methodName, count); - positionMap.set(method.getStart(), count); - // console.log(`[buildMethodPositionMap] Module class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); - }); - }); - - // Handle interfaces within the module - const interfaces = moduleNode.getInterfaces(); - interfaces.forEach(interfaceNode => { - // console.log(`[buildMethodPositionMap] Processing interface in module: ${interfaceNode.getName() || 'Unnamed'}`); - const methods = interfaceNode.getMethods(); - const methodCounts = new Map(); - - methods.forEach(method => { - const methodName = method.getName(); - const count = (methodCounts.get(methodName) || 0) + 1; - methodCounts.set(methodName, count); - positionMap.set(method.getStart(), count); - // console.log(`[buildMethodPositionMap] Module interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`); - }); - }); - - // Recursively process nested modules - const nestedModules = moduleNode.getModules(); - nestedModules.forEach(nestedModule => { - if (Node.isModuleDeclaration(nestedModule)) { - const nestedModuleName = nestedModule.getName(); - const newModulePath = `${modulePath}.${nestedModuleName}`; - processModule(nestedModule, newModulePath); - } - }); - - } - - function trackArrowFunctions(container: Node) { - const arrows = container.getDescendantsOfKind(SyntaxKind.ArrowFunction); - arrows.forEach(arrow => { - const parent = arrow.getParent(); - if (Node.isBlock(parent) || Node.isSourceFile(parent)) { - // Use negative numbers for arrow functions to distinguish from methods - positionMap.set(arrow.getStart(), -1 * (positionMap.size + 1)); - // console.log(`[buildMethodPositionMap] Arrow function at ${arrow.getStart()}`); - } - }); - } - - // Handle top-level classes - sourceFile.getClasses().forEach(classNode => { - // console.log(`[buildMethodPositionMap] Processing class: ${classNode.getName() || 'Unnamed'}`); - const methods = classNode.getMethods(); - const methodCounts = new Map(); - - methods.forEach(method => { - const methodName = method.getName(); - const count = (methodCounts.get(methodName) || 0) + 1; - methodCounts.set(methodName, count); - positionMap.set(method.getStart(), count); - // console.log(`[buildMethodPositionMap] Class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); - }); - - methods.forEach(method => trackArrowFunctions(method)); - }); - - // Handle top-level interfaces - sourceFile.getInterfaces().forEach(interfaceNode => { - // console.log(`[buildMethodPositionMap] Processing interface: ${interfaceNode.getName() || 'Unnamed'}`); - const methods = interfaceNode.getMethods(); - const methodCounts = new Map(); - - methods.forEach(method => { - const methodName = method.getName(); - const count = (methodCounts.get(methodName) || 0) + 1; - methodCounts.set(methodName, count); - positionMap.set(method.getStart(), count); - // console.log(`[buildMethodPositionMap] Interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`); - }); - methods.forEach(method => trackArrowFunctions(method)); - - }); - - // Handle top-level namespaces/modules - sourceFile.getModules().forEach(moduleNode => { - if (Node.isModuleDeclaration(moduleNode)) { - const moduleName = moduleNode.getName(); - processModule(moduleNode, moduleName); - } - }); - - - // console.log(`[buildMethodPositionMap] Final positionMap:`, Array.from(positionMap.entries())); - return positionMap; -} - -/** - * Generates a fully qualified name (FQN) for a given AST node. - * Constructs an FQN by traversing the node's ancestry, adding names and keys - * (numeric or string from object literals ...) as needed, prefixed with the file's relative path. - * - * @param node The AST node to generate an FQN for - * @returns A string representing the node's FQN (e.g., "{path}.operations.add.compute[MethodDeclaration]") - */ -export function getFQN(node: FQNNode | Node): string { - const sourceFile = node.getSourceFile(); - const absolutePathProject = entityDictionary.famixRep.getAbsolutePath(); - const parts: string[] = []; - let currentNode: Node | undefined = node; - - const stageMap = buildStageMethodMap(sourceFile); - const methodPositionMap = buildMethodPositionMap(sourceFile); - - while (currentNode && !Node.isSourceFile(currentNode)) { - const { line, column } = sourceFile.getLineAndColumnAtPos(currentNode.getStart()); - const lc = `${line}:${column}`; - - if (Node.isClassDeclaration(currentNode) || - Node.isClassExpression(currentNode) || - Node.isInterfaceDeclaration(currentNode) || - Node.isFunctionDeclaration(currentNode) || - Node.isMethodDeclaration(currentNode) || - Node.isModuleDeclaration(currentNode) || - Node.isVariableDeclaration(currentNode) || - Node.isGetAccessorDeclaration(currentNode) || - Node.isSetAccessorDeclaration(currentNode) || - Node.isPropertyDeclaration(currentNode) || - Node.isParameterDeclaration(currentNode) || - Node.isDecorator(currentNode) || - Node.isTypeAliasDeclaration(currentNode) || - Node.isEnumDeclaration(currentNode) || - Node.isEnumMember(currentNode) || - Node.isParametered(currentNode) || - Node.isPropertySignature(currentNode) || - Node.isArrayLiteralExpression(currentNode) || - Node.isImportSpecifier(currentNode) || - Node.isIdentifier(currentNode)) { - let name: string; - if (Node.isImportSpecifier(currentNode)) { - const alias = currentNode.getAliasNode()?.getText(); - if (alias) { - let importDecl: Node | undefined = currentNode; - while (importDecl && !Node.isImportDeclaration(importDecl)) { - importDecl = importDecl.getParent(); - } - const moduleSpecifier = importDecl && Node.isImportDeclaration(importDecl) - ? importDecl.getModuleSpecifier().getLiteralText() - : "unknown"; - name = currentNode.getName(); - name = `${name} as ${alias}[ImportSpecifier<${moduleSpecifier}>]`; - } else { - name = currentNode.getName(); - } - } else { - // if constructor, use "constructor" as name - if (Node.isConstructorDeclaration(currentNode)) { - name = "constructor"; - } else { - name = Node.isIdentifier(currentNode) ? currentNode.getText() - : 'getName' in currentNode && typeof currentNode['getName'] === 'function' - ? ((currentNode as { getName(): string }).getName() + - ((Node.isClassDeclaration(currentNode) || - Node.isInterfaceDeclaration(currentNode) || - Node.isMethodDeclaration(currentNode) || - Node.isFunctionDeclaration(currentNode)) && - 'getTypeParameters' in currentNode && - currentNode.getTypeParameters().length > 0 - ? getParameters(currentNode) - : '')) - : `Unnamed_${currentNode.getKindName()}(${lc})`; - } - } - - if (Node.isMethodSignature(currentNode)) { - const method = currentNode as MethodSignature; - const params = method.getParameters().map(p => { - const typeText = p.getType().getText().replace(/\s+/g, ""); - return typeText || "any"; // Fallback for untyped parameters - }); - const returnType = method.getReturnType().getText().replace(/\s+/g, "") || "void"; - name = `${name}(${params.join(",")}):${returnType}`; - } - - parts.unshift(name); - - // Apply positional index for MethodDeclaration, MethodSignature, and FunctionDeclaration - if (Node.isMethodDeclaration(currentNode) || - Node.isMethodSignature(currentNode) || - Node.isFunctionDeclaration(currentNode)) { - const key = stageMap.get(currentNode.getStart()); - if (key) { - parts.unshift(key); - // console.log(`[getFQN] Applied stageMap key: ${key} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`); - } else { - const positionIndex = methodPositionMap.get(currentNode.getStart()); - if (positionIndex && positionIndex > 1) { - parts.unshift(positionIndex.toString()); - // console.log(`[getFQN] Applied positionIndex: ${positionIndex} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`); - } else { - console.log(`[getFQN] No positionIndex applied for ${currentNode.getKindName()} at position ${currentNode.getStart()}, positionIndex: ${positionIndex || 'none'}`); - } - } - } - } - else if (Node.isArrowFunction(currentNode) || - Node.isBlock(currentNode) || - Node.isForInStatement(currentNode) || - Node.isForOfStatement(currentNode) || - Node.isForStatement(currentNode) || - Node.isCatchClause(currentNode)) { - const name = `${currentNode.getKindName()}(${lc})`; - parts.unshift(name); - } - else if (Node.isTypeParameterDeclaration(currentNode)) { - const arrowParent = currentNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction); - if (arrowParent) { - const arrowIndex = Math.abs(methodPositionMap.get(arrowParent.getStart()) || 0); - if (arrowIndex > 0) { - parts.unshift(arrowIndex.toString()); - } - } - parts.unshift(currentNode.getName()); - // Removed continue to allow ancestor processing - } - else if (Node.isConstructorDeclaration(currentNode)) { - const name = "constructor"; - parts.unshift(name); - } else { - console.log(`[getFQN] Ignoring node kind: ${currentNode.getKindName()}`); - } - - currentNode = currentNode.getParent(); - } - - let relativePath = entityDictionary.convertToRelativePath( - path.normalize(sourceFile.getFilePath()), - absolutePathProject - ).replace(/\\/g, "/"); - - // if (relativePath.includes("..")) { - // } - if (relativePath.startsWith("/")) { - relativePath = relativePath.slice(1); - } - parts.unshift(`{${relativePath}}`); - - const fqn = parts.join(".") + `[${node.getKindName()}]`; - // console.log(`[getFQN] Final FQN: ${fqn}`); - return fqn; -} - - -export function getUniqueFQN(node: Node): string | undefined { - const absolutePathProject = entityDictionary.famixRep.getAbsolutePath(); - const parts: string[] = []; - - if (node instanceof SourceFile) { - return entityDictionary.convertToRelativePath(path.normalize(node.getFilePath()), absolutePathProject).replace(/\\/g, "/"); - } - - let currentNode: Node | undefined = node; - while (currentNode) { - if (Node.isSourceFile(currentNode)) { - const relativePath = entityDictionary.convertToRelativePath(path.normalize(currentNode.getFilePath()), absolutePathProject).replace(/\\/g, "/"); - if (relativePath.includes("..")) { - logger.error(`Relative path contains ../: ${relativePath}`); - } - parts.unshift(relativePath); // Add file path at the start - break; - } else if (currentNode.getSymbol()) { - const name = currentNode.getSymbol()!.getName(); - // For anonymous nodes, use kind and position as unique identifiers - const identifier = name !== "__computed" ? name : `${currentNode.getKindName()}_${currentNode.getStartLinePos()}`; - parts.unshift(identifier); - } - currentNode = currentNode.getParent(); - } - - return parts.join("::"); -} - -/** - * Gets the name of a node, if it has one - * @param a A node - * @returns The name of the node, or an empty string if it doesn't have one - */ -export function getNameOfNode(a: Node): string { - let cKind: ClassDeclaration | undefined; - let iKind: InterfaceDeclaration | undefined; - let mKind: MethodDeclaration | undefined; - let fKind: FunctionDeclaration | undefined; - let alias: TSMorphTypeDeclaration | undefined; - switch (a.getKind()) { - case SyntaxKind.SourceFile: - return a.asKind(SyntaxKind.SourceFile)!.getBaseName(); - - case SyntaxKind.ModuleDeclaration: - return a.asKind(SyntaxKind.ModuleDeclaration)!.getName(); - - case SyntaxKind.ClassDeclaration: - cKind = a.asKind(SyntaxKind.ClassDeclaration); - if (cKind && cKind.getTypeParameters().length > 0) { - return cKind.getName() + getParameters(a); - } else { - return cKind?.getName() || ""; - } - - case SyntaxKind.InterfaceDeclaration: - iKind = a.asKind(SyntaxKind.InterfaceDeclaration); - if (iKind && iKind.getTypeParameters().length > 0) { - return iKind.getName() + getParameters(a); - } else { - return iKind?.getName() || ""; - } - - case SyntaxKind.PropertyDeclaration: - return a.asKind(SyntaxKind.PropertyDeclaration)!.getName(); - - case SyntaxKind.PropertySignature: - return a.asKind(SyntaxKind.PropertySignature)!.getName(); - - case SyntaxKind.MethodDeclaration: - mKind = a.asKind(SyntaxKind.MethodDeclaration); - if (mKind && mKind.getTypeParameters().length > 0) { - return mKind.getName() + getParameters(a); - } else { - return mKind?.getName() || ""; - } - - case SyntaxKind.MethodSignature: - return a.asKind(SyntaxKind.MethodSignature)!.getName(); - - case SyntaxKind.GetAccessor: - return a.asKind(SyntaxKind.GetAccessor)!.getName(); - - case SyntaxKind.SetAccessor: - return a.asKind(SyntaxKind.SetAccessor)!.getName(); - - case SyntaxKind.FunctionDeclaration: - fKind = a.asKind(SyntaxKind.FunctionDeclaration); - if (fKind && fKind.getTypeParameters().length > 0) { - return fKind.getName() + getParameters(a); - } else { - return fKind?.getName() || ""; - } - - case SyntaxKind.FunctionExpression: - return a.asKind(SyntaxKind.FunctionExpression)?.getName() || "anonymous"; - - case SyntaxKind.Parameter: - return a.asKind(SyntaxKind.Parameter)!.getName(); - - case SyntaxKind.VariableDeclaration: - return a.asKind(SyntaxKind.VariableDeclaration)!.getName(); - - case SyntaxKind.Decorator: - return "@" + a.asKind(SyntaxKind.Decorator)!.getName(); - - case SyntaxKind.TypeParameter: - return a.asKind(SyntaxKind.TypeParameter)!.getName(); - - case SyntaxKind.EnumDeclaration: - return a.asKind(SyntaxKind.EnumDeclaration)!.getName(); - - case SyntaxKind.EnumMember: - return a.asKind(SyntaxKind.EnumMember)!.getName(); - - case SyntaxKind.TypeAliasDeclaration: - // special case for parameterized types - alias = a.asKind(SyntaxKind.TypeAliasDeclaration); - if (alias && alias.getTypeParameters().length > 0) { - return alias.getName() + "<" + alias.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; - } - return a.asKind(SyntaxKind.TypeAliasDeclaration)!.getName(); - - case SyntaxKind.Constructor: - return "constructor"; - - default: - // throw new Error(`getNameOfNode called on a node that doesn't have a name: ${a.getKindName()}`); - // ancestor hasn't got a useful name - return ""; - } -} - -/** - * Gets the name of a node, if it has one - * @param a A node - * @returns The name of the node, or an empty string if it doesn't have one - */ -export function getParameters(a: Node): string { - let paramString = ""; - switch (a.getKind()) { - case SyntaxKind.ClassDeclaration: - paramString = "<" + a.asKind(SyntaxKind.ClassDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; - break; - case SyntaxKind.InterfaceDeclaration: - paramString = "<" + a.asKind(SyntaxKind.InterfaceDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; - break; - case SyntaxKind.MethodDeclaration: - paramString = "<" + a.asKind(SyntaxKind.MethodDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; - break; - case SyntaxKind.FunctionDeclaration: - paramString = "<" + a.asKind(SyntaxKind.FunctionDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; - break; - default: - throw new Error(`getParameters called on a node that doesn't have parameters: ${a.getKindName()}`); - } - return paramString; -} - -/** - * Gets the FQN of an unresolved interface that is being implemented or extended - * @param unresolvedInheritedClassOrInterface The expression with type arguments representing the interface - * @returns The FQN of the unresolved interface - */ -export function getFQNUnresolvedInheritedClassOrInterface(unresolvedInheritedClassOrInterface: ExpressionWithTypeArguments): string { - // Check for either ClassDeclaration or InterfaceDeclaration ancestor - const classAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.ClassDeclaration); - const interfaceAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.InterfaceDeclaration); - - // Validate the context - if (!classAncestor && !interfaceAncestor) { - throw new Error("getFQNUnresolvedClassOrInterface called on a node that is not in an implements or extends context"); - } - - // Check if it's a valid implements/extends context - let isValidContext = false; - - let classExtendsClass = false; - - if (classAncestor) { - // check if the class is extending or implementing an interface - const extendsClause = classAncestor.getExtends(); - const implementsClause = classAncestor.getImplements(); - isValidContext = (extendsClause !== undefined) || (implementsClause && implementsClause.length > 0); - classExtendsClass = extendsClause !== undefined; - } else if (interfaceAncestor) { - // Check extends clause for interfaces - const extendsClause = interfaceAncestor.getExtends(); - isValidContext = extendsClause && extendsClause.length > 0; - } - - if (!isValidContext) { - throw new Error("getFQNUnresolvedInterface called on a node that is not in a valid implements or extends context"); - } - - // get the name of the interface - const name = unresolvedInheritedClassOrInterface.getExpression().getText(); - - // Find where it's imported - search the entire source file - const sourceFile = unresolvedInheritedClassOrInterface.getSourceFile(); - const importDecls = sourceFile.getImportDeclarations(); - - for (const importDecl of importDecls) { - const moduleSpecifier = importDecl.getModuleSpecifierValue(); - const importClause = importDecl.getImportClause(); - - if (importClause) { - const namedImports = importClause.getNamedImports(); - // declarationName is ClassDeclaration if "class extends class" - const declarationName = classExtendsClass ? "ClassDeclaration" : "InterfaceDeclaration"; - - for (const namedImport of namedImports) { - if (namedImport.getName() === name) { - logger.debug(`Found import for ${name} in ${moduleSpecifier}`); - return `{module:${moduleSpecifier}}.${name}[${declarationName}]`; - } - } - } - } - - // If not found, return a default FQN format - return `{unknown-module}.${name}[InterfaceDeclaration]`; -} - +import { ArrowFunction, CallExpression, ClassDeclaration, ConstructorDeclaration, Decorator, EnumDeclaration, ExpressionWithTypeArguments, FunctionDeclaration, FunctionExpression, GetAccessorDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, Node, PropertyDeclaration, SetAccessorDeclaration, SourceFile, SyntaxKind, TypeParameterDeclaration, VariableDeclaration } from "ts-morph"; +import { logger } from "./analyze"; +import path from "path"; +import { TSMorphTypeDeclaration } from "./famix_functions/EntityDictionary"; +import { convertToRelativePath } from "./famix_functions/helpers_path"; + +type FQNNode = SourceFile | VariableDeclaration | ArrowFunction | Identifier | MethodDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | PropertyDeclaration | TSMorphTypeDeclaration | EnumDeclaration | ImportDeclaration | ImportEqualsDeclaration | CallExpression | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | Decorator | ModuleDeclaration; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function isFQNNode(node: Node): node is FQNNode { + return Node.isVariableDeclaration(node) || Node.isArrowFunction(node) || Node.isIdentifier(node) || Node.isMethodDeclaration(node) || Node.isClassDeclaration(node) || Node.isClassExpression(node) || Node.isDecorator(node) || Node.isModuleDeclaration(node) || Node.isCallExpression(node); +} + +/** + * Builds a map of method positions to their property keys in object literals. + * Scans all variable declarations in a source file, targeting object literals with any keys + * (e.g., `3: { method() {} }` or `add: { compute() {} }`), and maps each method's start position to its key. + * Logs each step for debugging. + * + * @param sourceFile The TypeScript source file to analyze + * @returns A Map where keys are method start positions and values are their property keys (e.g., "3", "add") + */ +function buildStageMethodMap(sourceFile: SourceFile): Map { + const stageMap = new Map(); + + sourceFile.getVariableDeclarations().forEach(varDecl => { + // const varName = varDecl.getName(); + const initializer = varDecl.getInitializer(); + + if (!initializer || !Node.isObjectLiteralExpression(initializer)) { + return; + } + + initializer.getProperties().forEach(prop => { + let key: string | undefined; + + if (Node.isPropertyAssignment(prop)) { + const nameNode = prop.getNameNode(); + + if (Node.isIdentifier(nameNode)) { + key = nameNode.getText(); + } else if (Node.isStringLiteral(nameNode)) { + key = nameNode.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); + } else if (Node.isNumericLiteral(nameNode)) { + key = nameNode.getText(); + } else if (Node.isComputedPropertyName(nameNode)) { + const expression = nameNode.getExpression(); + if (Node.isIdentifier(expression)) { + // Resolve variable value if possible + const symbol = expression.getSymbol(); + if (symbol) { + const decl = symbol.getDeclarations()[0]; + if (Node.isVariableDeclaration(decl) && decl.getInitializer()) { + const init = decl.getInitializer()!; + if (Node.isStringLiteral(init) || Node.isNumericLiteral(init)) { + key = init.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); + } + } + } + if (!key) { + key = expression.getText(); + } + } else if (Node.isBinaryExpression(expression) && expression.getOperatorToken().getText() === '+') { + // Handle simple string concatenation (e.g., "A" + "B") + const left = expression.getLeft(); + const right = expression.getRight(); + if (Node.isStringLiteral(left) && Node.isStringLiteral(right)) { + key = left.getLiteralText() + right.getLiteralText(); + } + } else if (Node.isTemplateExpression(expression)) { + // Handle template literals (e.g., `key-${1}`) + const head = expression.getHead().getLiteralText(); + const spans = expression.getTemplateSpans(); + if (spans.length === 1 && Node.isNumericLiteral(spans[0].getExpression())) { + const num = spans[0].getExpression().getText(); + key = `${head}${num}`; + } + } + if (!key) { + key = expression.getText(); // Fallback + } + } else { + return; + } + + const propInitializer = prop.getInitializer(); + if (propInitializer && Node.isObjectLiteralExpression(propInitializer)) { + propInitializer.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => { + // const methodName = method.getName(); + const pos = method.getStart(); + if (key) { + stageMap.set(pos, key); + } + }); + } + } + }); + }); + + return stageMap; +} + +/** + * Builds a map of method positions to their index in class/interface/namespace declarations + * @param sourceFile The TypeScript source file to analyze + * @returns A Map where keys are method start positions and values are their positional index (1-based) + */ +function buildMethodPositionMap(sourceFile: SourceFile): Map { + const positionMap = new Map(); + // console.log(`[buildMethodPositionMap] Starting analysis for file: ${sourceFile.getFilePath()}`); + + // Helper function to process modules recursively + function processModule(moduleNode: ModuleDeclaration, modulePath: string) { + // console.log(`[buildMethodPositionMap] Processing module: ${modulePath}`); + + // Handle functions directly in the module + const functions = moduleNode.getFunctions(); + const functionCounts = new Map(); + + functions.forEach(func => { + const funcName = func.getName() || `Unnamed_Function(${func.getStart()})`; + const count = (functionCounts.get(funcName) || 0) + 1; + functionCounts.set(funcName, count); + positionMap.set(func.getStart(), count); + // console.log(`[buildMethodPositionMap] Module function: ${funcName}, position: ${func.getStart()}, index: ${count}`); + }); + + // Handle classes within the module + const classes = moduleNode.getClasses(); + classes.forEach(classNode => { + // console.log(`[buildMethodPositionMap] Processing class in module: ${classNode.getName() || 'Unnamed'}`); + const methods = classNode.getMethods(); + const methodCounts = new Map(); + + methods.forEach(method => { + const methodName = method.getName(); + const count = (methodCounts.get(methodName) || 0) + 1; + methodCounts.set(methodName, count); + positionMap.set(method.getStart(), count); + // console.log(`[buildMethodPositionMap] Module class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); + }); + }); + + // Handle interfaces within the module + const interfaces = moduleNode.getInterfaces(); + interfaces.forEach(interfaceNode => { + // console.log(`[buildMethodPositionMap] Processing interface in module: ${interfaceNode.getName() || 'Unnamed'}`); + const methods = interfaceNode.getMethods(); + const methodCounts = new Map(); + + methods.forEach(method => { + const methodName = method.getName(); + const count = (methodCounts.get(methodName) || 0) + 1; + methodCounts.set(methodName, count); + positionMap.set(method.getStart(), count); + // console.log(`[buildMethodPositionMap] Module interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`); + }); + }); + + // Recursively process nested modules + const nestedModules = moduleNode.getModules(); + nestedModules.forEach(nestedModule => { + if (Node.isModuleDeclaration(nestedModule)) { + const nestedModuleName = nestedModule.getName(); + const newModulePath = `${modulePath}.${nestedModuleName}`; + processModule(nestedModule, newModulePath); + } + }); + + } + + function trackArrowFunctions(container: Node) { + const arrows = container.getDescendantsOfKind(SyntaxKind.ArrowFunction); + arrows.forEach(arrow => { + const parent = arrow.getParent(); + if (Node.isBlock(parent) || Node.isSourceFile(parent)) { + // Use negative numbers for arrow functions to distinguish from methods + positionMap.set(arrow.getStart(), -1 * (positionMap.size + 1)); + // console.log(`[buildMethodPositionMap] Arrow function at ${arrow.getStart()}`); + } + }); + } + + // Handle top-level classes + sourceFile.getClasses().forEach(classNode => { + // console.log(`[buildMethodPositionMap] Processing class: ${classNode.getName() || 'Unnamed'}`); + const methods = classNode.getMethods(); + const methodCounts = new Map(); + + methods.forEach(method => { + const methodName = method.getName(); + const count = (methodCounts.get(methodName) || 0) + 1; + methodCounts.set(methodName, count); + positionMap.set(method.getStart(), count); + // console.log(`[buildMethodPositionMap] Class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); + }); + + methods.forEach(method => trackArrowFunctions(method)); + }); + + // Handle top-level interfaces + sourceFile.getInterfaces().forEach(interfaceNode => { + // console.log(`[buildMethodPositionMap] Processing interface: ${interfaceNode.getName() || 'Unnamed'}`); + const methods = interfaceNode.getMethods(); + const methodCounts = new Map(); + + methods.forEach(method => { + const methodName = method.getName(); + const count = (methodCounts.get(methodName) || 0) + 1; + methodCounts.set(methodName, count); + positionMap.set(method.getStart(), count); + // console.log(`[buildMethodPositionMap] Interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`); + }); + methods.forEach(method => trackArrowFunctions(method)); + + }); + + // Handle top-level namespaces/modules + sourceFile.getModules().forEach(moduleNode => { + if (Node.isModuleDeclaration(moduleNode)) { + const moduleName = moduleNode.getName(); + processModule(moduleNode, moduleName); + } + }); + + + // console.log(`[buildMethodPositionMap] Final positionMap:`, Array.from(positionMap.entries())); + return positionMap; +} + +/** + * Generates a fully qualified name (FQN) for a given AST node. + * Constructs an FQN by traversing the node's ancestry, adding names and keys + * (numeric or string from object literals ...) as needed, prefixed with the file's relative path. + * + * @param node The AST node to generate an FQN for + * @returns A string representing the node's FQN (e.g., "{path}.operations.add.compute[MethodDeclaration]") + */ +export function getFQN(node: FQNNode | Node, absolutePathProject: string = ""): string { + const sourceFile = node.getSourceFile(); + const parts: string[] = []; + let currentNode: Node | undefined = node; + + const stageMap = buildStageMethodMap(sourceFile); + const methodPositionMap = buildMethodPositionMap(sourceFile); + + while (currentNode && !Node.isSourceFile(currentNode)) { + const { line, column } = sourceFile.getLineAndColumnAtPos(currentNode.getStart()); + const lc = `${line}:${column}`; + + if (Node.isClassDeclaration(currentNode) || + Node.isClassExpression(currentNode) || + Node.isInterfaceDeclaration(currentNode) || + Node.isFunctionDeclaration(currentNode) || + Node.isMethodDeclaration(currentNode) || + Node.isModuleDeclaration(currentNode) || + Node.isVariableDeclaration(currentNode) || + Node.isGetAccessorDeclaration(currentNode) || + Node.isSetAccessorDeclaration(currentNode) || + Node.isPropertyDeclaration(currentNode) || + Node.isParameterDeclaration(currentNode) || + Node.isDecorator(currentNode) || + Node.isTypeAliasDeclaration(currentNode) || + Node.isEnumDeclaration(currentNode) || + Node.isEnumMember(currentNode) || + Node.isParametered(currentNode) || + Node.isPropertySignature(currentNode) || + Node.isArrayLiteralExpression(currentNode) || + Node.isImportSpecifier(currentNode) || + Node.isIdentifier(currentNode)) { + let name: string; + if (Node.isImportSpecifier(currentNode)) { + const alias = currentNode.getAliasNode()?.getText(); + if (alias) { + let importDecl: Node | undefined = currentNode; + while (importDecl && !Node.isImportDeclaration(importDecl)) { + importDecl = importDecl.getParent(); + } + const moduleSpecifier = importDecl && Node.isImportDeclaration(importDecl) + ? importDecl.getModuleSpecifier().getLiteralText() + : "unknown"; + name = currentNode.getName(); + name = `${name} as ${alias}[ImportSpecifier<${moduleSpecifier}>]`; + } else { + name = currentNode.getName(); + } + } else { + // if constructor, use "constructor" as name + if (Node.isConstructorDeclaration(currentNode)) { + name = "constructor"; + } else { + name = Node.isIdentifier(currentNode) ? currentNode.getText() + : 'getName' in currentNode && typeof currentNode['getName'] === 'function' + ? ((currentNode as { getName(): string }).getName() + + ((Node.isClassDeclaration(currentNode) || + Node.isInterfaceDeclaration(currentNode) || + Node.isMethodDeclaration(currentNode) || + Node.isFunctionDeclaration(currentNode)) && + 'getTypeParameters' in currentNode && + currentNode.getTypeParameters().length > 0 + ? getParameters(currentNode) + : '')) + : `Unnamed_${currentNode.getKindName()}(${lc})`; + } + } + + if (Node.isMethodSignature(currentNode)) { + const method = currentNode as MethodSignature; + const params = method.getParameters().map(p => { + const typeText = p.getType().getText().replace(/\s+/g, ""); + return typeText || "any"; // Fallback for untyped parameters + }); + const returnType = method.getReturnType().getText().replace(/\s+/g, "") || "void"; + name = `${name}(${params.join(",")}):${returnType}`; + } + + parts.unshift(name); + + // Apply positional index for MethodDeclaration, MethodSignature, and FunctionDeclaration + if (Node.isMethodDeclaration(currentNode) || + Node.isMethodSignature(currentNode) || + Node.isFunctionDeclaration(currentNode)) { + const key = stageMap.get(currentNode.getStart()); + if (key) { + parts.unshift(key); + // console.log(`[getFQN] Applied stageMap key: ${key} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`); + } else { + const positionIndex = methodPositionMap.get(currentNode.getStart()); + if (positionIndex && positionIndex > 1) { + parts.unshift(positionIndex.toString()); + // console.log(`[getFQN] Applied positionIndex: ${positionIndex} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`); + } else { + console.log(`[getFQN] No positionIndex applied for ${currentNode.getKindName()} at position ${currentNode.getStart()}, positionIndex: ${positionIndex || 'none'}`); + } + } + } + } + else if (Node.isArrowFunction(currentNode) || + Node.isBlock(currentNode) || + Node.isForInStatement(currentNode) || + Node.isForOfStatement(currentNode) || + Node.isForStatement(currentNode) || + Node.isCatchClause(currentNode)) { + const name = `${currentNode.getKindName()}(${lc})`; + parts.unshift(name); + } + else if (Node.isTypeParameterDeclaration(currentNode)) { + const arrowParent = currentNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction); + if (arrowParent) { + const arrowIndex = Math.abs(methodPositionMap.get(arrowParent.getStart()) || 0); + if (arrowIndex > 0) { + parts.unshift(arrowIndex.toString()); + } + } + parts.unshift(currentNode.getName()); + // Removed continue to allow ancestor processing + } + else if (Node.isConstructorDeclaration(currentNode)) { + const name = "constructor"; + parts.unshift(name); + } else { + console.log(`[getFQN] Ignoring node kind: ${currentNode.getKindName()}`); + } + + currentNode = currentNode.getParent(); + } + + let relativePath = convertToRelativePath( + path.normalize(sourceFile.getFilePath()), + absolutePathProject + ).replace(/\\/g, "/"); + + // if (relativePath.includes("..")) { + // } + if (relativePath.startsWith("/")) { + relativePath = relativePath.slice(1); + } + parts.unshift(`{${relativePath}}`); + + const fqn = parts.join(".") + `[${node.getKindName()}]`; + // console.log(`[getFQN] Final FQN: ${fqn}`); + return fqn; +} + + +export function getUniqueFQN(node: Node, absolutePathProject: string = ""): string | undefined { + const parts: string[] = []; + + if (node instanceof SourceFile) { + return convertToRelativePath(path.normalize(node.getFilePath()), absolutePathProject).replace(/\\/g, "/"); + } + + let currentNode: Node | undefined = node; + while (currentNode) { + if (Node.isSourceFile(currentNode)) { + const relativePath = convertToRelativePath(path.normalize(currentNode.getFilePath()), absolutePathProject).replace(/\\/g, "/"); + if (relativePath.includes("..")) { + logger.error(`Relative path contains ../: ${relativePath}`); + } + parts.unshift(relativePath); // Add file path at the start + break; + } else if (currentNode.getSymbol()) { + const name = currentNode.getSymbol()!.getName(); + // For anonymous nodes, use kind and position as unique identifiers + const identifier = name !== "__computed" ? name : `${currentNode.getKindName()}_${currentNode.getStartLinePos()}`; + parts.unshift(identifier); + } + currentNode = currentNode.getParent(); + } + + return parts.join("::"); +} + +/** + * Gets the name of a node, if it has one + * @param a A node + * @returns The name of the node, or an empty string if it doesn't have one + */ +export function getNameOfNode(a: Node): string { + let cKind: ClassDeclaration | undefined; + let iKind: InterfaceDeclaration | undefined; + let mKind: MethodDeclaration | undefined; + let fKind: FunctionDeclaration | undefined; + let alias: TSMorphTypeDeclaration | undefined; + switch (a.getKind()) { + case SyntaxKind.SourceFile: + return a.asKind(SyntaxKind.SourceFile)!.getBaseName(); + + case SyntaxKind.ModuleDeclaration: + return a.asKind(SyntaxKind.ModuleDeclaration)!.getName(); + + case SyntaxKind.ClassDeclaration: + cKind = a.asKind(SyntaxKind.ClassDeclaration); + if (cKind && cKind.getTypeParameters().length > 0) { + return cKind.getName() + getParameters(a); + } else { + return cKind?.getName() || ""; + } + + case SyntaxKind.InterfaceDeclaration: + iKind = a.asKind(SyntaxKind.InterfaceDeclaration); + if (iKind && iKind.getTypeParameters().length > 0) { + return iKind.getName() + getParameters(a); + } else { + return iKind?.getName() || ""; + } + + case SyntaxKind.PropertyDeclaration: + return a.asKind(SyntaxKind.PropertyDeclaration)!.getName(); + + case SyntaxKind.PropertySignature: + return a.asKind(SyntaxKind.PropertySignature)!.getName(); + + case SyntaxKind.MethodDeclaration: + mKind = a.asKind(SyntaxKind.MethodDeclaration); + if (mKind && mKind.getTypeParameters().length > 0) { + return mKind.getName() + getParameters(a); + } else { + return mKind?.getName() || ""; + } + + case SyntaxKind.MethodSignature: + return a.asKind(SyntaxKind.MethodSignature)!.getName(); + + case SyntaxKind.GetAccessor: + return a.asKind(SyntaxKind.GetAccessor)!.getName(); + + case SyntaxKind.SetAccessor: + return a.asKind(SyntaxKind.SetAccessor)!.getName(); + + case SyntaxKind.FunctionDeclaration: + fKind = a.asKind(SyntaxKind.FunctionDeclaration); + if (fKind && fKind.getTypeParameters().length > 0) { + return fKind.getName() + getParameters(a); + } else { + return fKind?.getName() || ""; + } + + case SyntaxKind.FunctionExpression: + return a.asKind(SyntaxKind.FunctionExpression)?.getName() || "anonymous"; + + case SyntaxKind.Parameter: + return a.asKind(SyntaxKind.Parameter)!.getName(); + + case SyntaxKind.VariableDeclaration: + return a.asKind(SyntaxKind.VariableDeclaration)!.getName(); + + case SyntaxKind.Decorator: + return "@" + a.asKind(SyntaxKind.Decorator)!.getName(); + + case SyntaxKind.TypeParameter: + return a.asKind(SyntaxKind.TypeParameter)!.getName(); + + case SyntaxKind.EnumDeclaration: + return a.asKind(SyntaxKind.EnumDeclaration)!.getName(); + + case SyntaxKind.EnumMember: + return a.asKind(SyntaxKind.EnumMember)!.getName(); + + case SyntaxKind.TypeAliasDeclaration: + // special case for parameterized types + alias = a.asKind(SyntaxKind.TypeAliasDeclaration); + if (alias && alias.getTypeParameters().length > 0) { + return alias.getName() + "<" + alias.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; + } + return a.asKind(SyntaxKind.TypeAliasDeclaration)!.getName(); + + case SyntaxKind.Constructor: + return "constructor"; + + default: + // throw new Error(`getNameOfNode called on a node that doesn't have a name: ${a.getKindName()}`); + // ancestor hasn't got a useful name + return ""; + } +} + +/** + * Gets the name of a node, if it has one + * @param a A node + * @returns The name of the node, or an empty string if it doesn't have one + */ +export function getParameters(a: Node): string { + let paramString = ""; + switch (a.getKind()) { + case SyntaxKind.ClassDeclaration: + paramString = "<" + a.asKind(SyntaxKind.ClassDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; + break; + case SyntaxKind.InterfaceDeclaration: + paramString = "<" + a.asKind(SyntaxKind.InterfaceDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; + break; + case SyntaxKind.MethodDeclaration: + paramString = "<" + a.asKind(SyntaxKind.MethodDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; + break; + case SyntaxKind.FunctionDeclaration: + paramString = "<" + a.asKind(SyntaxKind.FunctionDeclaration)?.getTypeParameters().map(tp => tp.getName()).join(", ") + ">"; + break; + default: + throw new Error(`getParameters called on a node that doesn't have parameters: ${a.getKindName()}`); + } + return paramString; +} + +/** + * Gets the FQN of an unresolved interface that is being implemented or extended + * @param unresolvedInheritedClassOrInterface The expression with type arguments representing the interface + * @returns The FQN of the unresolved interface + */ +export function getFQNUnresolvedInheritedClassOrInterface(unresolvedInheritedClassOrInterface: ExpressionWithTypeArguments): string { + // Check for either ClassDeclaration or InterfaceDeclaration ancestor + const classAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.ClassDeclaration); + const interfaceAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.InterfaceDeclaration); + + // Validate the context + if (!classAncestor && !interfaceAncestor) { + throw new Error("getFQNUnresolvedClassOrInterface called on a node that is not in an implements or extends context"); + } + + // Check if it's a valid implements/extends context + let isValidContext = false; + + let classExtendsClass = false; + + if (classAncestor) { + // check if the class is extending or implementing an interface + const extendsClause = classAncestor.getExtends(); + const implementsClause = classAncestor.getImplements(); + isValidContext = (extendsClause !== undefined) || (implementsClause && implementsClause.length > 0); + classExtendsClass = extendsClause !== undefined; + } else if (interfaceAncestor) { + // Check extends clause for interfaces + const extendsClause = interfaceAncestor.getExtends(); + isValidContext = extendsClause && extendsClause.length > 0; + } + + if (!isValidContext) { + throw new Error("getFQNUnresolvedInterface called on a node that is not in a valid implements or extends context"); + } + + // get the name of the interface + const name = unresolvedInheritedClassOrInterface.getExpression().getText(); + + // Find where it's imported - search the entire source file + const sourceFile = unresolvedInheritedClassOrInterface.getSourceFile(); + const importDecls = sourceFile.getImportDeclarations(); + + for (const importDecl of importDecls) { + const moduleSpecifier = importDecl.getModuleSpecifierValue(); + const importClause = importDecl.getImportClause(); + + if (importClause) { + const namedImports = importClause.getNamedImports(); + // declarationName is ClassDeclaration if "class extends class" + const declarationName = classExtendsClass ? "ClassDeclaration" : "InterfaceDeclaration"; + + for (const namedImport of namedImports) { + if (namedImport.getName() === name) { + logger.debug(`Found import for ${name} in ${moduleSpecifier}`); + return `{module:${moduleSpecifier}}.${name}[${declarationName}]`; + } + } + } + } + + // If not found, return a default FQN format + return `{unknown-module}.${name}[InterfaceDeclaration]`; +} + diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index 0703f601..3140d338 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -17,30 +17,12 @@ export class FamixRepository { private famixFunctions = new Set(); // All Famix functions private famixFiles = new Set(); // All Famix files private idCounter = 1; // Id counter - private absolutePath: string = ""; - private fmxElementObjectMap = new Map(); private tsMorphObjectMap = new Map(); // TODO: add this map to have two-way mapping between Famix and TS Morph objects constructor() { this.addElement(new SourceLanguage()); // add the source language entity (TypeScript) } - public setFmxElementObjectMap(fmxElementObjectMap: Map) { - this.fmxElementObjectMap = fmxElementObjectMap; - } - - public getFmxElementObjectMap() { - return this.fmxElementObjectMap; - } - - public getAbsolutePath(): string { - return this.absolutePath; - } - - public setAbsolutePath(path: string) { - this.absolutePath = path; - } - /** * Gets a Famix entity by id * @param id An id of a Famix entity diff --git a/src/ts2famix-cli.ts b/src/ts2famix-cli.ts index 293883d7..d3987cd9 100644 --- a/src/ts2famix-cli.ts +++ b/src/ts2famix-cli.ts @@ -4,7 +4,6 @@ import yargs from "yargs"; import { Importer } from './analyze'; import { FamixRepository } from "./lib/famix/famix_repository"; import { Project } from "ts-morph"; -import { config } from "./analyze"; const argv = yargs .example(`ts2famix -i "path/to/project/**/*.ts" -o JSONModels/projectName.json`, 'Creates a JSON-format Famix model of typescript files.') @@ -26,9 +25,9 @@ const argv = yargs import { logger } from './analyze'; logger.settings.minLevel = Number(argv.loglevel) as number; -config.expectGraphemes = argv.graphemes as boolean; +const config = { expectGraphemes: argv.graphemes as boolean }; -const importer = new Importer(); +const importer = new Importer(config); let famixRep: FamixRepository; if ((argv.input as string).endsWith('tsconfig.json')) { diff --git a/test/entityDictionaryUnit.test.ts b/test/entityDictionaryUnit.test.ts index 02d0ef50..304c8d29 100644 --- a/test/entityDictionaryUnit.test.ts +++ b/test/entityDictionaryUnit.test.ts @@ -1,4 +1,4 @@ -import { entityDictionary } from "../src/analyze"; +import { EntityDictionary } from "../src/famix_functions/EntityDictionary"; import * as Famix from "../src/lib/famix/model/famix"; import { project } from './testUtils'; @@ -27,6 +27,8 @@ namespace MyNamespace { describe('EntityDictionary', () => { const modules = sourceFile.getModules(); + const config = { expectGraphemes: false }; + const entityDictionary = new EntityDictionary(config); test('should get a module/namespace and add it to the map', () => { diff --git a/test/importerInstances.test.ts b/test/importerInstances.test.ts new file mode 100644 index 00000000..734ea954 --- /dev/null +++ b/test/importerInstances.test.ts @@ -0,0 +1,198 @@ +import { Importer } from '../src/analyze'; +import { createProject } from './testUtils'; + +describe('Multiple Importer Instances', () => { + const sourceCode1 = ` + class Class1 { + property1: string; + method1() {} + } + `; + + const sourceCode2 = ` + class Class2 { + property2: number; + method2() {} + } + `; + + + it('should handle multiple importer instances independently', () => { + const importer1 = new Importer(); + const importer2 = new Importer(); + + const project1 = createProject(); + const project2 = createProject(); + + project1.createSourceFile('sourceCode1.ts', sourceCode1); + project2.createSourceFile('sourceCode2.ts', sourceCode2); + + const famixRep1 = importer1.famixRepFromProject(project1); + const famixRep2 = importer2.famixRepFromProject(project2); + + const class1 = famixRep1._getFamixClass('{sourceCode1.ts}.Class1[ClassDeclaration]'); + const class2 = famixRep2._getFamixClass('{sourceCode2.ts}.Class2[ClassDeclaration]'); + + expect(class1).not.toBeUndefined(); + expect(class2).not.toBeUndefined(); + + expect(famixRep1._getFamixClass('{sourceCode2.ts}.Class2[ClassDeclaration]')).toBeUndefined(); + expect(famixRep2._getFamixClass('{sourceCode1.ts}.Class1[ClassDeclaration]')).toBeUndefined(); + }); + + it('should handle multiple importers processing in parallel without interference', () => { + const importer1 = new Importer(); + const importer2 = new Importer(); + const importer3 = new Importer(); + + const project1 = createProject(); + const project2 = createProject(); + const project3 = createProject(); + + const complexSource1 = ` + class BaseClass { + baseMethod() {} + } + class DerivedClass extends BaseClass { + derivedMethod() {} + } + `; + + const complexSource2 = ` + interface ITest { + testMethod(): void; + } + class ImplementingClass implements ITest { + testMethod() {} + } + `; + + const complexSource3 = ` + class ClassWithAccess { + property: string = "test"; + method() { + const local = this.property; + } + } + `; + + project1.createSourceFile('sourceCode1.ts', complexSource1); + project2.createSourceFile('sourceCode2.ts', complexSource2); + project3.createSourceFile('sourceCode3.ts', complexSource3); + + const famixRep1 = importer1.famixRepFromProject(project1); + const famixRep2 = importer2.famixRepFromProject(project2); + const famixRep3 = importer3.famixRepFromProject(project3); + + const baseClassFqn = '{sourceCode1.ts}.BaseClass[ClassDeclaration]'; + const derivedClassFqn = '{sourceCode1.ts}.DerivedClass[ClassDeclaration]'; + const interfaceFqn = '{sourceCode2.ts}.ITest[InterfaceDeclaration]'; + const implementingClassFqn = '{sourceCode2.ts}.ImplementingClass[ClassDeclaration]'; + const classWithAccessFqn = '{sourceCode3.ts}.ClassWithAccess[ClassDeclaration]'; + + expect(famixRep1._getFamixClass(baseClassFqn)).not.toBeUndefined(); + expect(famixRep1._getFamixClass(derivedClassFqn)).not.toBeUndefined(); + expect(famixRep2._getFamixInterface(interfaceFqn)).not.toBeUndefined(); + expect(famixRep2._getFamixClass(implementingClassFqn)).not.toBeUndefined(); + + expect(famixRep3._getFamixClass(classWithAccessFqn)).not.toBeUndefined(); + + expect(famixRep1._getFamixInterface(interfaceFqn)).toBeUndefined(); + expect(famixRep2._getFamixClass(baseClassFqn)).toBeUndefined(); + expect(famixRep3._getFamixClass(derivedClassFqn)).toBeUndefined(); + }); + + it('should correctly handle imports between files in separate importers', () => { + const importer1 = new Importer(); + const importer2 = new Importer(); + + const project1 = createProject(); + const project2 = createProject(); + + const moduleA1 = ` + export class BaseModule { + sharedMethod() { + return 'shared functionality'; + } + } + `; + + const moduleB1 = ` + import { BaseModule } from './moduleA'; + + export class ExtendedModule extends BaseModule { + extendedMethod() { + return this.sharedMethod() + ' with extensions'; + } + } + `; + + const moduleA2 = ` + export interface IService { + execute(): void; + } + + export class DefaultService implements IService { + execute() { + console.log('Default implementation'); + } + } + `; + + const moduleB2 = ` + import { IService } from './moduleA'; + + export class CustomService implements IService { + execute() { + console.log('Custom implementation'); + } + } + + export class ServiceRegistry { + services: IService[] = []; + register(service: IService) { + this.services.push(service); + } + } + `; + + project1.createSourceFile('moduleA.ts', moduleA1); + project1.createSourceFile('moduleB.ts', moduleB1); + + project2.createSourceFile('moduleA.ts', moduleA2); + project2.createSourceFile('moduleB.ts', moduleB2); + + const famixRep1 = importer1.famixRepFromProject(project1); + const famixRep2 = importer2.famixRepFromProject(project2); + + const baseModuleFqn = '{moduleA.ts}.BaseModule[ClassDeclaration]'; + const extendedModuleFqn = '{moduleB.ts}.ExtendedModule[ClassDeclaration]'; + + const iServiceFqn = '{moduleA.ts}.IService[InterfaceDeclaration]'; + const defaultServiceFqn = '{moduleA.ts}.DefaultService[ClassDeclaration]'; + const customServiceFqn = '{moduleB.ts}.CustomService[ClassDeclaration]'; + const serviceRegistryFqn = '{moduleB.ts}.ServiceRegistry[ClassDeclaration]'; + + expect(famixRep1._getFamixClass(baseModuleFqn)).not.toBeUndefined(); + expect(famixRep1._getFamixClass(extendedModuleFqn)).not.toBeUndefined(); + + expect(famixRep2._getFamixInterface(iServiceFqn)).not.toBeUndefined(); + expect(famixRep2._getFamixClass(defaultServiceFqn)).not.toBeUndefined(); + expect(famixRep2._getFamixClass(customServiceFqn)).not.toBeUndefined(); + expect(famixRep2._getFamixClass(serviceRegistryFqn)).not.toBeUndefined(); + + expect(famixRep1._getFamixInterface(iServiceFqn)).toBeUndefined(); + expect(famixRep1._getFamixClass(customServiceFqn)).toBeUndefined(); + + expect(famixRep2._getFamixClass(baseModuleFqn)).toBeUndefined(); + expect(famixRep2._getFamixClass(extendedModuleFqn)).toBeUndefined(); + + const extendedModule = famixRep1._getFamixClass(extendedModuleFqn); + expect(extendedModule?.superInheritances.size).toBe(1); + + const customService = famixRep2._getFamixClass(customServiceFqn); + const defaultService = famixRep2._getFamixClass(defaultServiceFqn); + expect(customService?.superInheritances.size).toBe(1); + expect(defaultService?.superInheritances.size).toBe(1); + }); +}); \ No newline at end of file diff --git a/test/sourceText.test.ts b/test/sourceText.test.ts index 9fa86093..92e4e2b7 100644 --- a/test/sourceText.test.ts +++ b/test/sourceText.test.ts @@ -1,9 +1,10 @@ -import { Importer, config } from "../src/analyze"; +import { Importer } from "../src/analyze"; import { IndexedFileAnchor, Method, Module, ScriptEntity } from "../src/lib/famix/model/famix"; import GraphemeSplitter from "grapheme-splitter"; import { project } from './testUtils'; -const importer = new Importer(); +const config = { expectGraphemes: true }; +const importer = new Importer(config); project.createSourceFile("/test_src/simple.ts", `let a: number = 1; @@ -21,7 +22,6 @@ export class A { // multi-code point emoji is handled differently in JavaScript (two chars) and Pharo (one character) project.createSourceFile("/test_src/a-b.ts", `let c = "💷", d = 5;`); -config.expectGraphemes = true; const fmxRep = importer.famixRepFromProject(project); describe('Tests for source text', () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index 998cce1e..9e2f8583 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -2,14 +2,16 @@ import { IndexedFileAnchor } from "../src/lib/famix/model/famix/indexed_file_anc import { Comment } from "../src/lib/famix/model/famix/comment"; import { Project } from "ts-morph"; -export const project = new Project( - { +export const project = createProject(); + +export function createProject(): Project { + return new Project({ compilerOptions: { - baseUrl: "" + baseUrl: "", }, useInMemoryFileSystem: true, - } -); + }); +} function getIndexedFileAnchorFromComment(comment: Comment) { return comment?.sourceAnchor as IndexedFileAnchor;