From e033a2132ce29150da687b9bc9f906792df275c4 Mon Sep 17 00:00:00 2001 From: BELHADJ Mohamed El Amin Date: Tue, 8 Apr 2025 17:59:23 +0200 Subject: [PATCH 1/2] fix FQN issue specially with the object type literall index signature --- src/analyze.ts | 2 +- src/analyze_functions/process_functions.ts | 50 ++-- src/famix_functions/EntityDictionary.ts | 269 ++++++++------------ src/famix_functions/helpers_creation.ts | 36 ++- src/fqn.ts | 194 ++++++++++++-- test/ObjectLiteralIndexSignatureFQN.test.ts | 162 ++++++++++++ 6 files changed, 498 insertions(+), 215 deletions(-) create mode 100644 test/ObjectLiteralIndexSignatureFQN.test.ts diff --git a/src/analyze.ts b/src/analyze.ts index 9467c192..b4025c0a 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -6,7 +6,7 @@ import * as processFunctions from "./analyze_functions/process_functions"; import { EntityDictionary } from "./famix_functions/EntityDictionary"; import path from "path"; -export const logger = new Logger({ name: "ts2famix", minLevel: 3 }); +export const logger = new Logger({ name: "ts2famix", minLevel: 2 }); export const config = { "expectGraphemes": false }; export const entityDictionary = new EntityDictionary(); diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 0b068251..88076af9 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -78,19 +78,15 @@ export function getImplementedOrExtendedInterfaces(interfaces: Array): void { sourceFiles.forEach(file => { logger.info(`File: >>>>>>>>>> ${file.getFilePath()}`); - // Computes the cyclomatic complexity metrics for the current source file if it exists (i.e. if it is not from a jest test) - if (fs.existsSync(file.getFilePath())) + if (fs.existsSync(file.getFilePath())) { currentCC = calculate(file.getFilePath()); - else + } else { currentCC = {}; + } processFile(file); }); @@ -115,22 +111,18 @@ function processFile(f: SourceFile): void { logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`); processComments(f, fmxFile); - processAliases(f, fmxFile); - processClasses(f, fmxFile); - - processInterfaces(f, fmxFile); - - processVariables(f, fmxFile); - + processInterfaces(f, fmxFile); + processModules(f, fmxFile); + processVariables(f, fmxFile); // This will handle our object literal methods processEnums(f, fmxFile); - processFunctions(f, fmxFile); + - processModules(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; @@ -253,6 +245,26 @@ function processVariables(m: ContainerTypes, fmxScope: Famix.ScriptEntity | Fami fmxVariables.forEach(fmxVariable => { fmxScope.addVariable(fmxVariable); }); + + // Check each VariableDeclaration for object literal methods + v.getDeclarations().forEach(varDecl => { + const varName = varDecl.getName(); + console.log(`Checking variable: ${varName} at pos=${varDecl.getStart()}`); + const initializer = varDecl.getInitializer(); + if (initializer && Node.isObjectLiteralExpression(initializer)) { + initializer.getProperties().forEach(prop => { + if (Node.isPropertyAssignment(prop)) { + const nested = prop.getInitializer(); + if (nested && Node.isObjectLiteralExpression(nested)) { + nested.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => { + console.log(`Found object literal method: ${method.getName()} at pos=${method.getStart()}`); + entityDictionary.createOrGetFamixMethod(method, currentCC); + }); + } + } + }); + } + }); }); } @@ -868,7 +880,7 @@ export function processImportClausesForImportEqualsDeclarations(sourceFiles: Arr 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.getBaseName(); + const modulePath = module.getFilePath(); module.getImportDeclarations().forEach(impDecl => { logger.info(`Importing ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); const path = getModulePath(impDecl); @@ -877,6 +889,7 @@ export function processImportClausesForModules(modules: Array, expor 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, @@ -890,7 +903,7 @@ export function processImportClausesForModules(modules: Array, expor const defaultImport = impDecl.getDefaultImport(); if (defaultImport !== undefined) { logger.info(`Importing (default) ${defaultImport.getText()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); - // call with module, impDecl.getModuleSpecifierValue(), path, defaultImport, false, true + logger.debug(`Processing Default Import: ${defaultImport.getText()}, pos=${defaultImport.getStart()}`); entityDictionary.oldCreateOrGetFamixImportClause({ importDeclaration: impDecl, importerSourceFile: module, @@ -912,7 +925,6 @@ export function processImportClausesForModules(modules: Array, expor isInExports: false, isDefaultExport: false }); - // entityDictionary.createFamixImportClause(module, impDecl.getModuleSpecifierValue(), path, namespaceImport, false, false); } }); }); diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index 3355c928..7860e363 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -527,106 +527,77 @@ export class EntityDictionary { * @param currentCC The cyclomatic complexity metrics of the current source file * @returns The Famix model of the method or the accessor */ - public createOrGetFamixMethod(method: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration, currentCC: { [key: string]: number }): Famix.Method | Famix.Accessor | Famix.ParametricMethod { - let fmxMethod: Famix.Method | Famix.Accessor | Famix.ParametricMethod; - const isGeneric = method.getTypeParameters().length > 0; - const functionFullyQualifiedName = FQNFunctions.getFQN(method); - if (!this.fmxFunctionAndMethodMap.has(functionFullyQualifiedName)) { - + public createOrGetFamixMethod( + method: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration, + currentCC: { [key: string]: number } + ): Famix.Method | Famix.Accessor | Famix.ParametricMethod { + const fqn = FQNFunctions.getFQN(method); + logger.debug(`Processing method ${fqn}`); + + let fmxMethod = this.fmxFunctionAndMethodMap.get(fqn) as Famix.Method | Famix.Accessor | Famix.ParametricMethod; + if (!fmxMethod) { + const isGeneric = method.getTypeParameters().length > 0; if (method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) { fmxMethod = new Famix.Accessor(); const isGetter = method instanceof GetAccessorDeclaration; const isSetter = method instanceof SetAccessorDeclaration; - if (isGetter) {(fmxMethod as Famix.Accessor).kind = "getter";} - if (isSetter) {(fmxMethod as Famix.Accessor).kind = "setter";} - this.famixRep.addElement(fmxMethod); - } - else { - if (isGeneric) { - fmxMethod = new Famix.ParametricMethod(); - } - else { - fmxMethod = new Famix.Method(); - } + if (isGetter) { (fmxMethod as Famix.Accessor).kind = "getter"; } + if (isSetter) { (fmxMethod as Famix.Accessor).kind = "setter"; } + } else { + fmxMethod = isGeneric ? new Famix.ParametricMethod() : new Famix.Method(); } + const isConstructor = method instanceof ConstructorDeclaration; const isSignature = method instanceof MethodSignature; - let isAbstract = false; let isStatic = false; if (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) { isAbstract = method.isAbstract(); isStatic = method.isStatic(); } - - if (isConstructor) {(fmxMethod as Famix.Accessor).kind = "constructor";} + + if (isConstructor) { (fmxMethod as Famix.Accessor).kind = "constructor"; } fmxMethod.isAbstract = isAbstract; fmxMethod.isClassSide = isStatic; - fmxMethod.isPrivate = (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) ? (method.getModifiers().find(x => x.getText() === 'private')) !== undefined : false; - fmxMethod.isProtected = (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) ? (method.getModifiers().find(x => x.getText() === 'protected')) !== undefined : false; + fmxMethod.isPrivate = (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) + ? !!method.getModifiers().find(x => x.getText() === 'private') : false; + fmxMethod.isProtected = (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) + ? !!method.getModifiers().find(x => x.getText() === 'protected') : false; fmxMethod.signature = Helpers.computeSignature(method.getText()); - - let methodName: string; - if (isConstructor) { - methodName = "constructor"; - } - else { - methodName = (method as MethodDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration).getName(); - } + + const methodName = isConstructor ? "constructor" : method.getName(); fmxMethod.name = methodName; - - if (!isConstructor) { - if (method.getName().substring(0, 1) === "#") { - fmxMethod.isPrivate = true; - } - } - - if (!fmxMethod.isPrivate && !fmxMethod.isProtected) { - fmxMethod.isPublic = true; - } - else { - fmxMethod.isPublic = false; - } - - if (!isSignature) { - fmxMethod.cyclomaticComplexity = currentCC[fmxMethod.name]; - } - else { - fmxMethod.cyclomaticComplexity = 0; + + if (!isConstructor && methodName.startsWith("#")) { + fmxMethod.isPrivate = true; } - - let methodTypeName = this.UNKNOWN_VALUE; + fmxMethod.isPublic = !fmxMethod.isPrivate && !fmxMethod.isProtected; + + fmxMethod.cyclomaticComplexity = isSignature ? 0 : (currentCC[methodName] || 0); + let methodTypeName = this.UNKNOWN_VALUE; try { - methodTypeName = method.getReturnType().getText().trim(); + methodTypeName = method.getReturnType().getText().trim(); } catch (error) { - logger.error(`> WARNING: got exception ${error}. Failed to get usable name for return type of method: ${fmxMethod.name}. Continuing...`); + logger.error(`Failed to get return type for ${fqn}: ${error}`); } - + const fmxType = this.createOrGetFamixType(methodTypeName, method); fmxMethod.declaredType = fmxType; fmxMethod.numberOfLinesOfCode = method.getEndLineNumber() - method.getStartLineNumber(); - const parameters = method.getParameters(); - fmxMethod.numberOfParameters = parameters.length; - - if (!isSignature) { - fmxMethod.numberOfStatements = method.getStatements().length; - } - else { - fmxMethod.numberOfStatements = 0; - } - + fmxMethod.numberOfParameters = method.getParameters().length; + fmxMethod.numberOfStatements = isSignature ? 0 : method.getStatements().length; + + // Add to famixRep initFQN(method, fmxMethod); this.famixRep.addElement(fmxMethod); this.makeFamixIndexFileAnchor(method, fmxMethod); - - this.fmxFunctionAndMethodMap.set(functionFullyQualifiedName, fmxMethod); - } - else { - fmxMethod = this.fmxFunctionAndMethodMap.get(functionFullyQualifiedName) as (Famix.Method | Famix.Accessor | Famix.ParametricMethod); + this.fmxFunctionAndMethodMap.set(fqn, fmxMethod); + logger.debug(`Added method ${fqn} to famixRep`); + } else { + logger.debug(`Method ${fqn} already exists`); } - - this.fmxElementObjectMap.set(fmxMethod,method); - + + this.fmxElementObjectMap.set(fmxMethod, method); return fmxMethod; } @@ -975,72 +946,61 @@ export class EntityDictionary { * @param element A ts-morph element * @returns The Famix model of the type */ - public createOrGetFamixType(typeName: string, element: TSMorphTypeDeclaration): Famix.Type { - let fmxType: Famix.Type; +public createOrGetFamixType(typeName: string, element: TSMorphTypeDeclaration): Famix.Type { + logger.debug(`Creating (or getting) type: '${typeName}' of element: '${element?.getText().slice(0, 50)}...' of kind: ${element?.getKindName()}`); const isPrimitive = isPrimitiveType(typeName); const isParametricType = - element instanceof ClassDeclaration && element.getTypeParameters().length > 0 || - element instanceof InterfaceDeclaration && element.getTypeParameters().length > 0; - - // Functions and methods aren't types! - // || - // element instanceof FunctionDeclaration && element.getTypeParameters().length > 0 || - // element instanceof MethodDeclaration && element.getTypeParameters().length > 0 || - // element instanceof ArrowFunction && element.getTypeParameters().length > 0; - - logger.debug("Creating (or getting) type: '" + typeName + "' of element: " + element?.getText() + " of kind: " + element?.getKindName()); - + (element instanceof ClassDeclaration && element.getTypeParameters().length > 0) || + (element instanceof InterfaceDeclaration && element.getTypeParameters().length > 0); + if (isPrimitive) { return this.createOrGetFamixPrimitiveType(typeName); } - + if (isParametricType) { - // narrow the type - const parametricElement = element as TSMorphParametricType; - return this.createOrGetFamixParametricType(typeName, parametricElement); + return this.createOrGetFamixParametricType(typeName, element as TSMorphParametricType); } - + if (!this.fmxTypeMap.has(element)) { - let ancestor: Famix.ContainerEntity | undefined = undefined; - - if (element !== undefined) { + let ancestor: Famix.ContainerEntity | undefined; + if (element) { const typeAncestor = Helpers.findTypeAncestor(element); - if (!typeAncestor) { - throw new Error(`Ancestor not found for element ${element.getText()}.`); - } - const ancestorFullyQualifiedName = FQNFunctions.getFQN(typeAncestor); - ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity; - if (!ancestor) { - logger.debug(`Ancestor ${FQNFunctions.getFQN(typeAncestor)} not found. Adding the new type.`); - ancestor = this.createOrGetFamixType(typeAncestor.getText(), typeAncestor as TSMorphTypeDeclaration); + + if (typeAncestor) { + const ancestorFullyQualifiedName = FQNFunctions.getFQN(typeAncestor); + ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity; + if (!ancestor) { + ancestor = this.createOrGetFamixType(typeAncestor.getText(), typeAncestor as TSMorphTypeDeclaration); + } else { + console.log(`Found ancestor in famixRep: ${ancestor.fullyQualifiedName}`); + } + } else { + console.log(`No type ancestor found for ${typeName} - proceeding without container`); } } - - fmxType = new Famix.Type(); - fmxType.name = typeName; - - if (!ancestor) { + + const fmxType = new Famix.Type(); + fmxType.name = typeName; + if (ancestor) { + fmxType.container = ancestor; + } else { throw new Error(`Ancestor not found for type ${typeName}.`); } - fmxType.container = ancestor; + initFQN(element, fmxType); this.makeFamixIndexFileAnchor(element, fmxType); - this.famixRep.addElement(fmxType); - this.fmxTypeMap.set(element, fmxType); - } - else { - const result = this.fmxTypeMap.get(element); - if (result) { - fmxType = result; - } else { + } else { + const fmxType = this.fmxTypeMap.get(element); + if (!fmxType) { throw new Error(`Famix type ${typeName} is not found in the Type map.`); } + return fmxType; } - - this.fmxElementObjectMap.set(fmxType,element); - + + const fmxType = this.fmxTypeMap.get(element)!; + this.fmxElementObjectMap.set(fmxType, element); return fmxType; } @@ -1167,40 +1127,43 @@ export class EntityDictionary { if (!fmxVar) { throw new Error(`Famix entity with id ${id} not found, for node ${node.getText()} in ${node.getSourceFile().getBaseName()} at line ${node.getStartLineNumber()}.`); } - + logger.debug(`Creating FamixAccess. Node: [${node.getKindName()}] '${node.getText()}' at line ${node.getStartLineNumber()} in ${node.getSourceFile().getBaseName()}, id: ${id} refers to fmxVar '${fmxVar.fullyQualifiedName}'.`); - + const nodeReferenceAncestor = Helpers.findAncestor(node); + if (!nodeReferenceAncestor) { + logger.error(`No ancestor found for node '${node.getText()}'`); + return; + } + const ancestorFullyQualifiedName = FQNFunctions.getFQN(nodeReferenceAncestor); const accessor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity; if (!accessor) { logger.error(`Ancestor ${ancestorFullyQualifiedName} of kind ${nodeReferenceAncestor.getKindName()} not found.`); - // accessor = this.createOrGetFamixType(ancestorFullyQualifiedName, nodeReferenceAncestor as TypeDeclaration); - return; // bail out TODO: this is probably wrong + return; // Bail out for now } else { logger.debug(`Found accessor to be ${accessor.fullyQualifiedName}.`); } - - - // make sure accessor is a method, function, script or module + + // Ensure accessor is a method, function, script, or module if (!(accessor instanceof Famix.Method) && !(accessor instanceof Famix.ArrowFunction) && !(accessor instanceof Famix.Function) && !(accessor instanceof Famix.ScriptEntity) && !(accessor instanceof Famix.Module)) { logger.error(`Accessor ${accessor.fullyQualifiedName} is not a method, function, etc.`); return; } - - // don't create any duplicates (e.g. if the same variable is accessed multiple times by same accessor) + + // Avoid duplicates const foundAccess = this.famixRep.getFamixAccessByAccessorAndVariable(accessor, fmxVar); if (foundAccess) { logger.debug(`FamixAccess already exists for accessor ${accessor.fullyQualifiedName} and variable ${fmxVar.fullyQualifiedName}.`); return; } + const fmxAccess = new Famix.Access(); fmxAccess.accessor = accessor; fmxAccess.variable = fmxVar; - this.famixRep.addElement(fmxAccess); - - this.fmxElementObjectMap.set(fmxAccess,node); + this.fmxElementObjectMap.set(fmxAccess, node); + logger.debug(`Created access: ${accessor.fullyQualifiedName} -> ${fmxVar.fullyQualifiedName}`); } /** @@ -1379,39 +1342,36 @@ export class EntityDictionary { if (importDeclaration && this.fmxImportClauseMap.has(importDeclaration)) { const rImportClause = this.fmxImportClauseMap.get(importDeclaration); if (rImportClause) { - return; // don't do anything + logger.debug(`Import clause ${importElement.getText()} already exists in map, skipping.`); + return; } else { throw new Error(`Import clause ${importElement.getText()} is not found in the import clause map.`); } } - + logger.info(`creating a new FamixImportClause for ${importDeclaration?.getText()} in ${importer.getBaseName()}.`); const fmxImportClause = new Famix.ImportClause(); - + let importedEntity: Famix.NamedEntity | Famix.StructuralEntity | undefined = undefined; let importedEntityName: string; - + const absolutePathProject = this.famixRep.getAbsolutePath(); const absolutePath = path.normalize(moduleSpecifierFilePath); - // convert the path and remove any windows backslashes introduced by path.normalize 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: pathInProject: ${pathInProject}`); let pathName = "{" + pathInProject + "}."; logger.debug(`createFamixImportClause: pathName: ${pathName}`); - - // Named imports, e.g. import { ClassW } from "./complexExportModule"; - - // Start with simple import clause (without referring to the actual variable) - + if (importDeclaration instanceof ImportDeclaration && importElement instanceof ImportSpecifier) { importedEntityName = importElement.getName(); pathName = pathName + importedEntityName; if (isInExports) { importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(pathName) as Famix.NamedEntity; + logger.debug(`Found exported entity: ${pathName}`); } if (importedEntity === undefined) { importedEntity = new Famix.NamedEntity(); @@ -1419,38 +1379,37 @@ export class EntityDictionary { if (!isInExports) { importedEntity.isStub = true; } - // logger.debug(`createFamixImportClause: Creating named entity ${importedEntityName} with FQN ${pathName}`); - // importedEntity.fullyQualifiedName = pathName; + logger.debug(`Creating named entity ${importedEntityName} for ImportSpecifier ${importElement.getText()}`); initFQN(importElement, importedEntity); - + logger.debug(`Assigned FQN to entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); - // must add entity to repository this.famixRep.addElement(importedEntity); + logger.debug(`Added entity to repository: ${importedEntity.fullyQualifiedName}`); } } - // handle import equals declarations, e.g. import myModule = require("./complexExportModule"); - // TypeScript can't determine the type of the imported module, so we create a Module entity else if (importDeclaration instanceof ImportEqualsDeclaration) { importedEntityName = importDeclaration?.getName(); pathName = pathName + importedEntityName; importedEntity = new Famix.StructuralEntity(); importedEntity.name = importedEntityName; initFQN(importDeclaration, importedEntity); + logger.debug(`Assigned FQN to ImportEquals entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); - // importedEntity.fullyQualifiedName = pathName; const anyType = this.createOrGetFamixType('any', importDeclaration); (importedEntity as Famix.StructuralEntity).declaredType = anyType; - } else { // default imports, e.g. import ClassW from "./complexExportModule"; + } else { importedEntityName = importElement.getText(); pathName = pathName + (isDefaultExport ? "defaultExport" : "namespaceExport"); importedEntity = new Famix.NamedEntity(); importedEntity.name = importedEntityName; - // importedEntity.fullyQualifiedName = pathName; initFQN(importElement, importedEntity); + logger.debug(`Assigned FQN to default/namespace entity: ${importedEntity.fullyQualifiedName}`); this.makeFamixIndexFileAnchor(importElement, importedEntity); } - // I don't think it should be added to the repository if it exists already - if (!isInExports) this.famixRep.addElement(importedEntity); + if (!isInExports) { + this.famixRep.addElement(importedEntity); + logger.debug(`Added non-exported entity to repository: ${importedEntity.fullyQualifiedName}`); + } const importerFullyQualifiedName = FQNFunctions.getFQN(importer); const fmxImporter = this.famixRep.getFamixEntityByFullyQualifiedName(importerFullyQualifiedName) as Famix.Module; fmxImportClause.importingEntity = fmxImporter; @@ -1461,13 +1420,11 @@ export class EntityDictionary { fmxImportClause.moduleSpecifier = importDeclaration?.getModuleSpecifierValue() as string; } - logger.debug(`createFamixImportClause: ${fmxImportClause.importedEntity?.name} (of type ${ - Helpers.getSubTypeName(fmxImportClause.importedEntity)}) is imported by ${fmxImportClause.importingEntity?.name}`); - + logger.debug(`ImportClause: ${fmxImportClause.importedEntity?.name} (type=${Helpers.getSubTypeName(fmxImportClause.importedEntity)}) imported by ${fmxImportClause.importingEntity?.name}`); + fmxImporter.addOutgoingImport(fmxImportClause); - this.famixRep.addElement(fmxImportClause); - + if (importDeclaration) { this.fmxElementObjectMap.set(fmxImportClause, importDeclaration); this.fmxImportClauseMap.set(importDeclaration, fmxImportClause); diff --git a/src/famix_functions/helpers_creation.ts b/src/famix_functions/helpers_creation.ts index dea6eced..ae9276ec 100644 --- a/src/famix_functions/helpers_creation.ts +++ b/src/famix_functions/helpers_creation.ts @@ -87,23 +87,31 @@ export function findAncestor(node: Identifier): Node { * @param element A ts-morph element * @returns The ancestor of the ts-morph element */ -export function findTypeAncestor(element: TSMorphTypeDeclaration): Node { +export function findTypeAncestor(element: Node): Node | undefined { let ancestor: Node | undefined; - ancestor = element.getAncestors().find(a => - a.getKind() === SyntaxKind.MethodDeclaration || - a.getKind() === SyntaxKind.Constructor || - a.getKind() === SyntaxKind.MethodSignature || - a.getKind() === SyntaxKind.FunctionDeclaration || - a.getKind() === SyntaxKind.FunctionExpression || - a.getKind() === SyntaxKind.ModuleDeclaration || - a.getKind() === SyntaxKind.SourceFile || - a.getKindName() === "GetAccessor" || - a.getKindName() === "SetAccessor" || - a.getKind() === SyntaxKind.ClassDeclaration || - a.getKind() === SyntaxKind.InterfaceDeclaration); + const ancestors = element.getAncestors(); + console.log(`Ancestors count: ${ancestors.length}`); + + ancestor = ancestors.find(a => { + const kind = a.getKind(); + const kindName = a.getKindName(); + return kind === SyntaxKind.MethodDeclaration || + kind === SyntaxKind.Constructor || + kind === SyntaxKind.MethodSignature || + kind === SyntaxKind.FunctionDeclaration || + kind === SyntaxKind.FunctionExpression || + kind === SyntaxKind.ModuleDeclaration || + kind === SyntaxKind.SourceFile || + kindName === "GetAccessor" || + kindName === "SetAccessor" || + kind === SyntaxKind.ClassDeclaration || + kind === SyntaxKind.InterfaceDeclaration; + }); + if (!ancestor) { throw new Error(`Type ancestor not found for ${element.getKindName()}`); - } + } + return ancestor; } diff --git a/src/fqn.ts b/src/fqn.ts index 561ca466..c7c8df2a 100644 --- a/src/fqn.ts +++ b/src/fqn.ts @@ -10,16 +10,126 @@ 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); } -export function getFQN(node: FQNNode | Node): string { - const absolutePathProject = entityDictionary.famixRep.getAbsolutePath(); +/** + * 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)) { + console.log(` Debug: ${varName} initializer not an object literal`); + return; + } + + initializer.getProperties().forEach(prop => { + let key: string | undefined; + if (Node.isPropertyAssignment(prop)) { + const nameNode = prop.getNameNode(); + + if (Node.isIdentifier(nameNode)) { + key = nameNode.getText(); + console.log(` Debug: Found identifier key=${key}`); + } else if (Node.isStringLiteral(nameNode)) { + key = nameNode.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); + console.log(` Debug: Found string literal key=${key}`); + } else if (Node.isNumericLiteral(nameNode)) { + key = nameNode.getText(); + console.log(` Debug: Found numeric literal key=${key}`); + } 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'); + console.log(` Debug: Resolved computed identifier key=${key}`); + } + } + } + if (!key) { + key = expression.getText(); + console.log(` Debug: Fallback computed identifier key=${key}`); + } + } 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(); + console.log(` Debug: Evaluated concat key=${key}`); + } + } 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}`; + console.log(` Debug: Evaluated template key=${key}`); + } + } + if (!key) { + key = expression.getText(); // Fallback + console.log(` Debug: Fallback computed key=${key}`); + } + } 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); + console.log(` Debug: Mapped key=${key} to ${methodName} @ pos=${pos}`); + } + }); + } + } + }); + }); + + return stageMap; +} + +/** + * 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); + 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) || @@ -41,44 +151,78 @@ export function getFQN(node: FQNNode | Node): string { Node.isArrayLiteralExpression(currentNode) || Node.isImportSpecifier(currentNode) || Node.isIdentifier(currentNode)) { - const name = Node.isIdentifier(currentNode) ? currentNode.getText() - : getNameOfNode(currentNode) /* currentNode.getName() */ || 'Unnamed_' + currentNode.getKindName() + `(${lc})`; + 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 { + name = Node.isIdentifier(currentNode) ? currentNode.getText() + : (currentNode as any).getName?.() || `Unnamed_${currentNode.getKindName()}(${lc})`; + } parts.unshift(name); - } - // unnamed nodes + console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); + + if (Node.isMethodDeclaration(currentNode)) { + const key = stageMap.get(currentNode.getStart()); + if (key) { + console.log(` Found key=${key} for ${name} @ pos=${currentNode.getStart()} from map`); + parts.unshift(key); + console.log(` Added key=${key}, parts=${JSON.stringify(parts)}`); + const parentFQN = parts.slice(0, -1).join("."); + console.log(` Debug: Method FQN=${parentFQN}.${name}[${node.getKindName()}], Parent FQN=${parentFQN}`); + } else { + console.log(` No key mapped for ${name} @ pos=${currentNode.getStart()}`); + } + } + } else if (Node.isArrowFunction(currentNode) || - Node.isBlock(currentNode) || - Node.isForInStatement(currentNode) || - Node.isForOfStatement(currentNode) || - Node.isForStatement(currentNode) || - Node.isCatchClause(currentNode)) { - parts.unshift(`${currentNode.getKindName()}(${lc})`); - } else if (Node.isConstructorDeclaration(currentNode)) { - parts.unshift(`constructor`); + Node.isBlock(currentNode) || + Node.isForInStatement(currentNode) || + Node.isForOfStatement(currentNode) || + Node.isForStatement(currentNode) || + Node.isCatchClause(currentNode)) { + const name = `${currentNode.getKindName()}(${lc})`; + parts.unshift(name); + console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); + } + else if (Node.isConstructorDeclaration(currentNode)) { + const name = "constructor"; + parts.unshift(name); + console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); } else { - // For other kinds, you might want to handle them specifically or ignore - logger.debug(`Ignoring node kind: ${currentNode.getKindName()}`); + console.log(`Ignoring node kind: ${currentNode.getKindName()}`); } + currentNode = currentNode.getParent(); } - - - // Prepend the relative path of the source file let relativePath = entityDictionary.convertToRelativePath( path.normalize(sourceFile.getFilePath()), - absolutePathProject).replace(/\\/sg, "/"); - + absolutePathProject + ).replace(/\\/g, "/"); + if (relativePath.includes("..")) { - logger.error(`Relative path contains ../: ${relativePath}`); + console.log(`Relative path contains ../: ${relativePath}`); } if (relativePath.startsWith("/")) { - relativePath = relativePath.substring(1); + relativePath = relativePath.slice(1); } parts.unshift(`{${relativePath}}`); - const fqn = parts.join(".") + `[${node.getKindName()}]`; // disambiguate - logger.debug(`Generated FQN: ${fqn} for node: ${node.getKindName()}`); + const fqn = parts.join(".") + `[${node.getKindName()}]`; + console.log(`getFQN End: fqn=${fqn}, parts=${JSON.stringify(parts)}, node=${node.getText().slice(0, 50)}..., kind=${node.getKindName()}`); return fqn; } diff --git a/test/ObjectLiteralIndexSignatureFQN.test.ts b/test/ObjectLiteralIndexSignatureFQN.test.ts new file mode 100644 index 00000000..5f62ff6a --- /dev/null +++ b/test/ObjectLiteralIndexSignatureFQN.test.ts @@ -0,0 +1,162 @@ +import { Project, SyntaxKind } from 'ts-morph'; +import { getFQN } from '../src/fqn'; +import { Importer } from '../src/analyze'; +import * as Famix from '../src/lib/famix/model/famix'; + +const project = new Project({ + compilerOptions: { + baseUrl: "" + }, + useInMemoryFileSystem: true, +}); + +describe('Object Literal Index Signature FQN Generation', () => { + let sourceFile: ReturnType; + let importer: Importer; + let fmxRep: any; + + beforeAll(() => { + sourceFile = project.createSourceFile('/ObjectLiteralIndexSignatureFQN.ts', ` + const key1 = Symbol('key1'); + const key2 = "varString"; + const key3 = 42; + export const object1 = { + 1: { + method1() {}, + method2() {} + }, + "keyString": { + method3() {}, + method4() {} + }, + ["prefix" + "Key"]: { + method5() {} + }, + [key1]: { + method6() {} + }, + [\`template\${7}\`]: { + method7() {} + }, + [key2]: { + method8() {} + }, + [key3]: { + method9() {} + } + }; + `); + + importer = new Importer(); + fmxRep = importer.famixRepFromProject(project); + }); + + it('should parse the source file and generate Famix representation', () => { + expect(fmxRep).toBeTruthy(); + expect(sourceFile).toBeTruthy(); + }); + + it('should contain the object1 variable with correct FQN', () => { + const objectDecl = sourceFile.getVariableDeclaration('object1'); + expect(objectDecl).toBeDefined(); + expect(getFQN(objectDecl!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1[VariableDeclaration]'); + }); + + it('should generate correct FQN for numeric key methods', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method1 = methods.find(m => m.getName() === 'method1'); + expect(method1).toBeDefined(); + expect(getFQN(method1!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.1.method1[MethodDeclaration]'); + + const method2 = methods.find(m => m.getName() === 'method2'); + expect(method2).toBeDefined(); + expect(getFQN(method2!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.1.method2[MethodDeclaration]'); + + const famixMethod1 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.1.method1[MethodDeclaration]'); + expect(famixMethod1).toBeTruthy(); + expect(famixMethod1.name).toBe('method1'); + + const famixMethod2 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.1.method2[MethodDeclaration]'); + expect(famixMethod2).toBeTruthy(); + expect(famixMethod2.name).toBe('method2'); + }); + + it('should generate correct FQN for string literal key methods', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method3 = methods.find(m => m.getName() === 'method3'); + expect(method3).toBeDefined(); + expect(getFQN(method3!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.keyString.method3[MethodDeclaration]'); + + const method4 = methods.find(m => m.getName() === 'method4'); + expect(method4).toBeDefined(); + expect(getFQN(method4!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.keyString.method4[MethodDeclaration]'); + + const famixMethod3 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.keyString.method3[MethodDeclaration]'); + expect(famixMethod3).toBeTruthy(); + expect(famixMethod3.name).toBe('method3'); + + const famixMethod4 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.keyString.method4[MethodDeclaration]'); + expect(famixMethod4).toBeTruthy(); + expect(famixMethod4.name).toBe('method4'); + }); + + it('should generate correct FQN for computed property (string concat) method', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method5 = methods.find(m => m.getName() === 'method5'); + expect(method5).toBeDefined(); + expect(getFQN(method5!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.prefixKey.method5[MethodDeclaration]'); + + const famixMethod5 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.prefixKey.method5[MethodDeclaration]'); + expect(famixMethod5).toBeTruthy(); + expect(famixMethod5.name).toBe('method5'); + }); + + it('should generate correct FQN for symbol key method', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method6 = methods.find(m => m.getName() === 'method6'); + expect(method6).toBeDefined(); + expect(getFQN(method6!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.key1.method6[MethodDeclaration]'); + + const famixMethod6 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.key1.method6[MethodDeclaration]'); + expect(famixMethod6).toBeTruthy(); + expect(famixMethod6.name).toBe('method6'); + }); + + it('should generate correct FQN for template literal key method', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method7 = methods.find(m => m.getName() === 'method7'); + expect(method7).toBeDefined(); + expect(getFQN(method7!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.template7.method7[MethodDeclaration]'); + + const famixMethod7 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.template7.method7[MethodDeclaration]'); + expect(famixMethod7).toBeTruthy(); + expect(famixMethod7.name).toBe('method7'); + }); + + it('should generate correct FQN for dynamic string variable key method', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method8 = methods.find(m => m.getName() === 'method8'); + expect(method8).toBeDefined(); + expect(getFQN(method8!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.varString.method8[MethodDeclaration]'); + + const famixMethod8 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.varString.method8[MethodDeclaration]'); + expect(famixMethod8).toBeTruthy(); + expect(famixMethod8.name).toBe('method8'); + }); + + it('should generate correct FQN for dynamic numeric variable key method', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const method9 = methods.find(m => m.getName() === 'method9'); + expect(method9).toBeDefined(); + expect(getFQN(method9!)).toBe('{ObjectLiteralIndexSignatureFQN.ts}.object1.42.method9[MethodDeclaration]'); + + const famixMethod9 = fmxRep._getFamixMethod('{ObjectLiteralIndexSignatureFQN.ts}.object1.42.method9[MethodDeclaration]'); + expect(famixMethod9).toBeTruthy(); + expect(famixMethod9.name).toBe('method9'); + }); + + it('should have the correct number of methods in the Famix representation', () => { + const famixMethods = fmxRep._getAllEntitiesWithType('Method') as Set; + expect(famixMethods.size).toBe(9); + }); +}); \ No newline at end of file From 97aa1258088a8c08e51bbe8d7078732a0a839a2b Mon Sep 17 00:00:00 2001 From: BELHADJ Mohamed El Amin Date: Tue, 29 Apr 2025 13:38:25 +0200 Subject: [PATCH 2/2] Fix FQN Collisions for Method Signatures and Overloaded Methods --- src/famix_functions/EntityDictionary.ts | 36 ++- src/fqn.ts | 78 +++++-- test/MethodOverloadFQN.test.ts | 296 ++++++++++++++++++++++++ test/MethodSignatureFQN.test.ts | 144 ++++++++++++ test/fqn.test.ts | 6 +- 5 files changed, 534 insertions(+), 26 deletions(-) create mode 100644 test/MethodOverloadFQN.test.ts create mode 100644 test/MethodSignatureFQN.test.ts diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index 7860e363..d90888fb 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -530,9 +530,11 @@ export class EntityDictionary { public createOrGetFamixMethod( method: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration, currentCC: { [key: string]: number } - ): Famix.Method | Famix.Accessor | Famix.ParametricMethod { + ): Famix.Method | Famix.Accessor | Famix.ParametricMethod { + const fqn = FQNFunctions.getFQN(method); logger.debug(`Processing method ${fqn}`); + let fmxMethod = this.fmxFunctionAndMethodMap.get(fqn) as Famix.Method | Famix.Accessor | Famix.ParametricMethod; if (!fmxMethod) { @@ -948,6 +950,36 @@ export class EntityDictionary { */ public createOrGetFamixType(typeName: string, element: TSMorphTypeDeclaration): Famix.Type { logger.debug(`Creating (or getting) type: '${typeName}' of element: '${element?.getText().slice(0, 50)}...' of kind: ${element?.getKindName()}`); + if (element.isKind(SyntaxKind.MethodSignature) || element.isKind(SyntaxKind.MethodDeclaration)) { + const methodFQN = FQNFunctions.getFQN(element); + const returnTypeFQN = `${methodFQN.replace(/\[Method(Signature|Declaration)\]$/, '')}[ReturnType]`; + + // Check if we already have this return type in the repository + const existingType = this.famixRep.getFamixEntityByFullyQualifiedName(returnTypeFQN); + if (existingType) { + return existingType as Famix.Type; + } + + const fmxType = new Famix.Type(); + fmxType.name = typeName; + fmxType.fullyQualifiedName = returnTypeFQN; + + // Set container (same as method's container) + const methodAncestor = Helpers.findTypeAncestor(element); + if (methodAncestor) { + const ancestorFQN = FQNFunctions.getFQN(methodAncestor); + const ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFQN) as Famix.ContainerEntity; + if (ancestor) { + fmxType.container = ancestor; + } + } + + this.famixRep.addElement(fmxType); + this.fmxTypeMap.set(element, fmxType); + this.fmxElementObjectMap.set(fmxType, element); + return fmxType; + } + const isPrimitive = isPrimitiveType(typeName); const isParametricType = (element instanceof ClassDeclaration && element.getTypeParameters().length > 0) || @@ -980,7 +1012,7 @@ public createOrGetFamixType(typeName: string, element: TSMorphTypeDeclaration): } const fmxType = new Famix.Type(); - fmxType.name = typeName; + fmxType.name = typeName; if (ancestor) { fmxType.container = ancestor; } else { diff --git a/src/fqn.ts b/src/fqn.ts index c7c8df2a..7af8fe33 100644 --- a/src/fqn.ts +++ b/src/fqn.ts @@ -27,7 +27,6 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { const initializer = varDecl.getInitializer(); if (!initializer || !Node.isObjectLiteralExpression(initializer)) { - console.log(` Debug: ${varName} initializer not an object literal`); return; } @@ -39,13 +38,10 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { if (Node.isIdentifier(nameNode)) { key = nameNode.getText(); - console.log(` Debug: Found identifier key=${key}`); } else if (Node.isStringLiteral(nameNode)) { key = nameNode.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); - console.log(` Debug: Found string literal key=${key}`); } else if (Node.isNumericLiteral(nameNode)) { key = nameNode.getText(); - console.log(` Debug: Found numeric literal key=${key}`); } else if (Node.isComputedPropertyName(nameNode)) { const expression = nameNode.getExpression(); @@ -58,13 +54,11 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { const init = decl.getInitializer()!; if (Node.isStringLiteral(init) || Node.isNumericLiteral(init)) { key = init.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1'); - console.log(` Debug: Resolved computed identifier key=${key}`); } } } if (!key) { key = expression.getText(); - console.log(` Debug: Fallback computed identifier key=${key}`); } } else if (Node.isBinaryExpression(expression) && expression.getOperatorToken().getText() === '+') { // Handle simple string concatenation (e.g., "A" + "B") @@ -72,7 +66,6 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { const right = expression.getRight(); if (Node.isStringLiteral(left) && Node.isStringLiteral(right)) { key = left.getLiteralText() + right.getLiteralText(); - console.log(` Debug: Evaluated concat key=${key}`); } } else if (Node.isTemplateExpression(expression)) { // Handle template literals (e.g., `key-${1}`) @@ -81,12 +74,10 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { if (spans.length === 1 && Node.isNumericLiteral(spans[0].getExpression())) { const num = spans[0].getExpression().getText(); key = `${head}${num}`; - console.log(` Debug: Evaluated template key=${key}`); } } if (!key) { key = expression.getText(); // Fallback - console.log(` Debug: Fallback computed key=${key}`); } } else { return; @@ -99,7 +90,6 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { const pos = method.getStart(); if (key) { stageMap.set(pos, key); - console.log(` Debug: Mapped key=${key} to ${methodName} @ pos=${pos}`); } }); } @@ -110,6 +100,45 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { return stageMap; } +/** + * Builds a map of method positions to their index in class/interface 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(); + + // Handle classes + sourceFile.getClasses().forEach(classNode => { + 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); + }); + }); + + // Handle interfaces + sourceFile.getInterfaces().forEach(interfaceNode => { + 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); + }); + }); + + 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 @@ -125,6 +154,7 @@ export function getFQN(node: FQNNode | Node): 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()); @@ -171,19 +201,30 @@ export function getFQN(node: FQNNode | Node): string { name = Node.isIdentifier(currentNode) ? currentNode.getText() : (currentNode as any).getName?.() || `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); - console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); - if (Node.isMethodDeclaration(currentNode)) { + if (Node.isMethodDeclaration(currentNode) || Node.isMethodSignature(currentNode)) { const key = stageMap.get(currentNode.getStart()); if (key) { - console.log(` Found key=${key} for ${name} @ pos=${currentNode.getStart()} from map`); parts.unshift(key); - console.log(` Added key=${key}, parts=${JSON.stringify(parts)}`); - const parentFQN = parts.slice(0, -1).join("."); - console.log(` Debug: Method FQN=${parentFQN}.${name}[${node.getKindName()}], Parent FQN=${parentFQN}`); } else { - console.log(` No key mapped for ${name} @ pos=${currentNode.getStart()}`); + // Check if this is a method that needs positional index + const positionIndex = methodPositionMap.get(currentNode.getStart()); + if (positionIndex && positionIndex > 1) { + // Only add position if it's not the first occurrence (backward compatibility) + parts.unshift(positionIndex.toString()); + } } } } @@ -195,12 +236,10 @@ export function getFQN(node: FQNNode | Node): string { Node.isCatchClause(currentNode)) { const name = `${currentNode.getKindName()}(${lc})`; parts.unshift(name); - console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); } else if (Node.isConstructorDeclaration(currentNode)) { const name = "constructor"; parts.unshift(name); - console.log(` Step: text=${currentNode.getText().slice(0, 50)}..., kind=${currentNode.getKindName()}, pos=${currentNode.getStart()}, name=${name}, parts=${JSON.stringify(parts)}`); } else { console.log(`Ignoring node kind: ${currentNode.getKindName()}`); } @@ -222,7 +261,6 @@ export function getFQN(node: FQNNode | Node): string { parts.unshift(`{${relativePath}}`); const fqn = parts.join(".") + `[${node.getKindName()}]`; - console.log(`getFQN End: fqn=${fqn}, parts=${JSON.stringify(parts)}, node=${node.getText().slice(0, 50)}..., kind=${node.getKindName()}`); return fqn; } diff --git a/test/MethodOverloadFQN.test.ts b/test/MethodOverloadFQN.test.ts new file mode 100644 index 00000000..16d75763 --- /dev/null +++ b/test/MethodOverloadFQN.test.ts @@ -0,0 +1,296 @@ +import { Project, SyntaxKind } from 'ts-morph'; +import { getFQN } from '../src/fqn'; +import { Importer } from '../src/analyze'; +import * as Famix from '../src/lib/famix/model/famix'; + +const project = new Project({ + compilerOptions: { + baseUrl: "" + }, + useInMemoryFileSystem: true, +}); + +describe('Method Overload and Parameter FQN Generation', () => { + let sourceFile: ReturnType; + let importer: Importer; + let fmxRep: any; + + beforeAll(() => { + sourceFile = project.createSourceFile('/MethodOverloadFQN.ts', ` + declare namespace ns1 { + class Calculator { + static add(x: string): number; + static add(x: number): number; + static add(x: any): number; + } + interface ICalculator { + subtract(value: string): number; + subtract(value: number): number; + } + } + declare namespace ns2 { + class Processor { + static process(data: boolean): void; + static process(data: null): void; + } + } + declare namespace monaco { + interface UriComponents { + scheme: string; + authority: string; + } + class Uri { + static revive(data: UriComponents | Uri): Uri; + static revive(data: UriComponents | Uri | undefined): Uri | undefined; + static revive(data: UriComponents | Uri | null): Uri | null; + static revive(data: UriComponents | Uri | undefined | null): Uri | undefined | null; + } + } + `); + + importer = new Importer(); + fmxRep = importer.famixRepFromProject(project); + }); + + it('should parse the source file and generate Famix representation', () => { + expect(fmxRep).toBeTruthy(); + expect(sourceFile).toBeTruthy(); + }); + + it('should generate correct FQNs for class methods in namespace ns1.Calculator', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const add1 = methods.find(m => m.getName() === 'add' && m.getParameters()[0]?.getType().getText() === 'string'); + expect(add1).toBeDefined(); + expect(getFQN(add1!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.add[MethodDeclaration]'); + + const add2 = methods.find(m => m.getName() === 'add' && m.getParameters()[0]?.getType().getText() === 'number'); + expect(add2).toBeDefined(); + expect(getFQN(add2!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.2.add[MethodDeclaration]'); + + const add3 = methods.find(m => m.getName() === 'add' && m.getParameters()[0]?.getType().getText() === 'any'); + expect(add3).toBeDefined(); + expect(getFQN(add3!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.3.add[MethodDeclaration]'); + + const famixAdd1 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns1.Calculator.add[MethodDeclaration]'); + expect(famixAdd1).toBeTruthy(); + expect(famixAdd1.name).toBe('add'); + + const famixAdd2 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns1.Calculator.2.add[MethodDeclaration]'); + expect(famixAdd2).toBeTruthy(); + expect(famixAdd2.name).toBe('add'); + + const famixAdd3 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns1.Calculator.3.add[MethodDeclaration]'); + expect(famixAdd3).toBeTruthy(); + expect(famixAdd3.name).toBe('add'); + }); + + it('should generate correct FQNs for interface methods in namespace ns1.ICalculator', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const subtract1 = methods.find(m => m.getName() === 'subtract' && m.getParameters()[0]?.getType().getText() === 'string'); + expect(subtract1).toBeDefined(); + expect(getFQN(subtract1!)).toBe('{MethodOverloadFQN.ts}.ns1.ICalculator.subtract(string):number[MethodSignature]'); + + const subtract2 = methods.find(m => m.getName() === 'subtract' && m.getParameters()[0]?.getType().getText() === 'number'); + expect(subtract2).toBeDefined(); + expect(getFQN(subtract2!)).toBe('{MethodOverloadFQN.ts}.ns1.ICalculator.2.subtract(number):number[MethodSignature]'); + + const famixSubtract1 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns1.ICalculator.subtract(string):number[MethodSignature]'); + expect(famixSubtract1).toBeTruthy(); + expect(famixSubtract1.name).toBe('subtract'); + + const famixSubtract2 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns1.ICalculator.2.subtract(number):number[MethodSignature]'); + expect(famixSubtract2).toBeTruthy(); + expect(famixSubtract2.name).toBe('subtract'); + }); + + it('should generate correct FQNs for parameters in ns1.Calculator.add', () => { + const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); + const x1 = parameters.find(p => p.getName() === 'x' && p.getType().getText() === 'string'); + expect(x1).toBeDefined(); + expect(getFQN(x1!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.add.x[Parameter]'); + + const x2 = parameters.find(p => p.getName() === 'x' && p.getType().getText() === 'number'); + expect(x2).toBeDefined(); + expect(getFQN(x2!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.2.add.x[Parameter]'); + + const x3 = parameters.find(p => p.getName() === 'x' && p.getType().getText() === 'any'); + expect(x3).toBeDefined(); + expect(getFQN(x3!)).toBe('{MethodOverloadFQN.ts}.ns1.Calculator.3.add.x[Parameter]'); + + const famixParameters = fmxRep._getAllEntitiesWithType('Parameter') as Set; + const famixParam1 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns1.Calculator.add.x[Parameter]'); + expect(famixParam1).toBeTruthy(); + expect(famixParam1).toBeDefined(); + expect(famixParam1!.name).toBe('x'); + + const famixParam2 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns1.Calculator.2.add.x[Parameter]'); + expect(famixParam2).toBeTruthy(); + expect(famixParam2).toBeDefined(); + if (famixParam2) { + expect(famixParam2.name).toBe('x'); + } + + const famixParam3 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns1.Calculator.3.add.x[Parameter]'); + expect(famixParam3).toBeTruthy(); + if (famixParam3) { + expect(famixParam3.name).toBe('x'); + } + }); + + it('should generate correct FQNs for parameters in ns1.ICalculator.subtract', () => { + const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); + const value1 = parameters.find(p => p.getName() === 'value' && p.getType().getText() === 'string'); + expect(value1).toBeDefined(); + expect(getFQN(value1!)).toBe('{MethodOverloadFQN.ts}.ns1.ICalculator.subtract(string):number.value[Parameter]'); + + const value2 = parameters.find(p => p.getName() === 'value' && p.getType().getText() === 'number'); + expect(value2).toBeDefined(); + expect(getFQN(value2!)).toBe('{MethodOverloadFQN.ts}.ns1.ICalculator.2.subtract(number):number.value[Parameter]'); + + const famixParameters = fmxRep._getAllEntitiesWithType('Parameter') as Set; + const famixParam1 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns1.ICalculator.subtract(string):number.value[Parameter]'); + expect(famixParam1).toBeTruthy(); + expect(famixParam1).toBeDefined(); + if (famixParam1) { + expect(famixParam1.name).toBe('value'); + } + + const famixParam2 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns1.ICalculator.2.subtract(number):number.value[Parameter]'); + expect(famixParam2).toBeTruthy(); + if (famixParam2) { + expect(famixParam2.name).toBe('value'); + } + }); + it('should generate correct FQNs for class methods in namespace ns2.Processor', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const process1 = methods.find(m => m.getName() === 'process' && m.getParameters()[0]?.getType().getText() === 'boolean'); + expect(process1).toBeDefined(); + expect(getFQN(process1!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.process[MethodDeclaration]'); + + const process2 = methods.find(m => m.getName() === 'process' && m.getParameters()[0]?.getType().getText() === 'null'); + expect(process2).toBeDefined(); + expect(getFQN(process2!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.2.process[MethodDeclaration]'); + + const famixProcess1 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns2.Processor.process[MethodDeclaration]'); + expect(famixProcess1).toBeTruthy(); + expect(famixProcess1.name).toBe('process'); + + const famixProcess2 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.ns2.Processor.2.process[MethodDeclaration]'); + expect(famixProcess2).toBeTruthy(); + expect(famixProcess2.name).toBe('process'); + }); + + it('should generate correct FQNs for parameters in ns2.Processor.process', () => { + const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); + const data1 = parameters.find(p => p.getName() === 'data' && p.getType().getText() === 'boolean'); + expect(data1).toBeDefined(); + expect(getFQN(data1!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.process.data[Parameter]'); + + const data2 = parameters.find(p => p.getName() === 'data' && p.getType().getText() === 'null'); + expect(data2).toBeDefined(); + expect(getFQN(data2!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.2.process.data[Parameter]'); + + const famixParameters = fmxRep._getAllEntitiesWithType('Parameter') as Set; + const famixParam1 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns2.Processor.process.data[Parameter]'); + expect(famixParam1).toBeTruthy(); + expect(famixParam1?.name).toBe('data'); + + const famixParam2 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns2.Processor.2.process.data[Parameter]'); + expect(famixParam2).toBeTruthy(); + if (famixParam2) { + expect(famixParam2.name).toBe('data'); + } + }); + + it('should generate correct FQNs for parameters in ns2.Processor.process', () => { + const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); + const data1 = parameters.find(p => p.getName() === 'data' && p.getType().getText() === 'boolean'); + expect(data1).toBeDefined(); + expect(getFQN(data1!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.process.data[Parameter]'); + + const data2 = parameters.find(p => p.getName() === 'data' && p.getType().getText() === 'null'); + expect(data2).toBeDefined(); + expect(getFQN(data2!)).toBe('{MethodOverloadFQN.ts}.ns2.Processor.2.process.data[Parameter]'); + + const famixParameters = fmxRep._getAllEntitiesWithType('Parameter') as Set; + const famixParam1 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns2.Processor.process.data[Parameter]'); + expect(famixParam1).toBeTruthy(); + expect(famixParam1?.name).toBe('data'); + + const famixParam2 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.ns2.Processor.2.process.data[Parameter]'); + expect(famixParam2).toBeTruthy(); + expect(famixParam2?.name).toBe('data'); + }); + + it('should generate correct FQNs for class methods in namespace monaco.Uri', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); + const revive1 = methods.find(m => m.getName() === 'revive' && m.getText().includes('data: UriComponents | Uri): Uri')); + expect(revive1).toBeDefined(); + expect(getFQN(revive1!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.revive[MethodDeclaration]'); + + const revive2 = methods.find(m => m.getName() === 'revive' && m.getText().includes('data: UriComponents | Uri | undefined): Uri | undefined')); + expect(revive2).toBeDefined(); + expect(getFQN(revive2!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.2.revive[MethodDeclaration]'); + + const revive3 = methods.find(m => m.getName() === 'revive' && m.getText().includes('data: UriComponents | Uri | null): Uri | null')); + expect(revive3).toBeDefined(); + expect(getFQN(revive3!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.3.revive[MethodDeclaration]'); + + const revive4 = methods.find(m => m.getName() === 'revive' && m.getText().includes('data: UriComponents | Uri | undefined | null): Uri | undefined | null')); + expect(revive4).toBeDefined(); + expect(getFQN(revive4!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.4.revive[MethodDeclaration]'); + + const famixRevive1 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.monaco.Uri.revive[MethodDeclaration]'); + expect(famixRevive1).toBeTruthy(); + expect(famixRevive1.name).toBe('revive'); + + const famixRevive2 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.monaco.Uri.2.revive[MethodDeclaration]'); + expect(famixRevive2).toBeTruthy(); + expect(famixRevive2.name).toBe('revive'); + + const famixRevive3 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.monaco.Uri.3.revive[MethodDeclaration]'); + expect(famixRevive3).toBeTruthy(); + expect(famixRevive3.name).toBe('revive'); + + const famixRevive4 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.monaco.Uri.4.revive[MethodDeclaration]'); + expect(famixRevive4).toBeTruthy(); + expect(famixRevive4.name).toBe('revive'); + }); + + it('should generate correct FQNs for parameters in monaco.Uri.revive', () => { + const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); + const data1 = parameters.find(p => p.getName() === 'data' && p.getParent().getText().includes('data: UriComponents | Uri): Uri')); + expect(data1).toBeDefined(); + expect(getFQN(data1!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.revive.data[Parameter]'); + + const data2 = parameters.find(p => p.getName() === 'data' && p.getParent().getText().includes('data: UriComponents | Uri | undefined): Uri | undefined')); + expect(data2).toBeDefined(); + expect(getFQN(data2!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.2.revive.data[Parameter]'); + + const data3 = parameters.find(p => p.getName() === 'data' && p.getParent().getText().includes('data: UriComponents | Uri | null): Uri | null')); + expect(data3).toBeDefined(); + expect(getFQN(data3!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.3.revive.data[Parameter]'); + + const data4 = parameters.find(p => p.getName() === 'data' && p.getParent().getText().includes('data: UriComponents | Uri | undefined | null): Uri | undefined | null')); + expect(data4).toBeDefined(); + expect(getFQN(data4!)).toBe('{MethodOverloadFQN.ts}.monaco.Uri.4.revive.data[Parameter]'); + + const famixParameters = fmxRep._getAllEntitiesWithType('Parameter') as Set; + const famixParam1 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.monaco.Uri.revive.data[Parameter]'); + expect(famixParam1).toBeTruthy(); + expect(famixParam1?.name).toBe('data'); + + const famixParam2 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.monaco.Uri.2.revive.data[Parameter]'); + expect(famixParam2).toBeTruthy(); + expect(famixParam2?.name).toBe('data'); + + const famixParam3 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.monaco.Uri.3.revive.data[Parameter]'); + expect(famixParam3).toBeTruthy(); + expect(famixParam3?.name).toBe('data'); + + const famixParam4 = Array.from(famixParameters).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.monaco.Uri.4.revive.data[Parameter]'); + expect(famixParam4).toBeTruthy(); + expect(famixParam4?.name).toBe('data'); + }); + +}); \ No newline at end of file diff --git a/test/MethodSignatureFQN.test.ts b/test/MethodSignatureFQN.test.ts new file mode 100644 index 00000000..6106cb46 --- /dev/null +++ b/test/MethodSignatureFQN.test.ts @@ -0,0 +1,144 @@ +import { Project, SyntaxKind } from 'ts-morph'; +import { getFQN } from '../src/fqn'; +import { Importer } from '../src/analyze'; +import * as Famix from '../src/lib/famix/model/famix'; + +const project = new Project({ + compilerOptions: { + baseUrl: "" + }, + useInMemoryFileSystem: true, +}); + +describe('Method Signature FQN Generation with Return Type', () => { + let sourceFile: ReturnType; + let importer: Importer; + let fmxRep: any; + + beforeAll(() => { + sourceFile = project.createSourceFile('/SourceFile1.ts', ` + interface GenericType {} + interface Interface1 { + method1( + param1: GenericType | Function | string | symbol + ): TOutput; + method1( + param1: GenericType | Function | string | symbol, + param2: { strict?: boolean; each?: undefined | false } + ): TOutput; + method1( + param1: GenericType | Function | string | symbol, + param2: { strict?: boolean; each: true } + ): Array; + } + `); + + importer = new Importer(); + fmxRep = importer.famixRepFromProject(project); + }); + + it('should parse the source file and generate Famix representation', () => { + expect(fmxRep).toBeTruthy(); + expect(sourceFile).toBeTruthy(); + }); + + it('should contain the Interface1 interface with correct FQN', () => { + const interfaceDecl = sourceFile.getInterface('Interface1'); + expect(interfaceDecl).toBeDefined(); + expect(getFQN(interfaceDecl!)).toBe('{SourceFile1.ts}.Interface1[InterfaceDeclaration]'); + }); + + it('should generate correct FQN for first method1 signature', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method1 = methods[0]; + expect(method1).toBeDefined(); + expect(getFQN(method1!)).toBe('{SourceFile1.ts}.Interface1.method1(string|symbol|Function|GenericType):TOutput[MethodSignature]'); + + const famixMethod1 = fmxRep._getFamixMethod('{SourceFile1.ts}.Interface1.method1(string|symbol|Function|GenericType):TOutput[MethodSignature]'); + expect(famixMethod1).toBeTruthy(); + expect(famixMethod1.name).toBe('method1'); + }); + + it('should generate correct FQN for first method1 return type', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method1 = methods[0]; + expect(method1).toBeDefined(); + + const returnTypeFQN = '{SourceFile1.ts}.Interface1.method1(string|symbol|Function|GenericType):TOutput[ReturnType]'; + const famixReturnType = fmxRep.getFamixEntityByFullyQualifiedName(returnTypeFQN); + expect(famixReturnType).toBeTruthy(); + expect(famixReturnType.name).toBe('TOutput'); + }); + + it('should generate correct FQN for second method1 signature', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method2 = methods[1]; + expect(method2).toBeDefined(); + expect(getFQN(method2!)).toBe('{SourceFile1.ts}.Interface1.2.method1(string|symbol|Function|GenericType,{strict?:boolean;each?:false;}):TOutput[MethodSignature]'); + + const famixMethod2 = fmxRep._getFamixMethod('{SourceFile1.ts}.Interface1.2.method1(string|symbol|Function|GenericType,{strict?:boolean;each?:false;}):TOutput[MethodSignature]'); + expect(famixMethod2).toBeTruthy(); + expect(famixMethod2.name).toBe('method1'); + }); + + it('should generate correct FQN for second method1 return type', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method2 = methods[1]; + expect(method2).toBeDefined(); + + const returnTypeFQN = '{SourceFile1.ts}.Interface1.2.method1(string|symbol|Function|GenericType,{strict?:boolean;each?:false;}):TOutput[ReturnType]'; + const famixReturnType = fmxRep.getFamixEntityByFullyQualifiedName(returnTypeFQN); + expect(famixReturnType).toBeTruthy(); + expect(famixReturnType.name).toBe('TOutput'); + }); + + it('should generate correct FQN for third method1 signature', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method3 = methods[2]; + expect(method3).toBeDefined(); + expect(getFQN(method3!)).toBe('{SourceFile1.ts}.Interface1.3.method1(string|symbol|Function|GenericType,{strict?:boolean;each:true;}):TOutput[][MethodSignature]'); + + const famixMethod3 = fmxRep._getFamixMethod('{SourceFile1.ts}.Interface1.3.method1(string|symbol|Function|GenericType,{strict?:boolean;each:true;}):TOutput[][MethodSignature]'); + expect(famixMethod3).toBeTruthy(); + expect(famixMethod3.name).toBe('method1'); + }); + + it('should generate correct FQN for third method1 return type', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + const method3 = methods[2]; + expect(method3).toBeDefined(); + + const returnTypeFQN = '{SourceFile1.ts}.Interface1.3.method1(string|symbol|Function|GenericType,{strict?:boolean;each:true;}):TOutput[][ReturnType]'; + const famixReturnType = fmxRep.getFamixEntityByFullyQualifiedName(returnTypeFQN); + expect(famixReturnType).toBeTruthy(); + expect(famixReturnType.name).toBe('TOutput[]'); + }); + + it('should generate correct FQN for method parameters', () => { + const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); + + // First method parameters + const method1 = methods[0]; + const param1 = method1.getParameters()[0]; + expect(param1).toBeDefined(); + expect(getFQN(param1!)).toBe('{SourceFile1.ts}.Interface1.method1(string|symbol|Function|GenericType):TOutput.param1[Parameter]'); + + // Second method parameters + const method2 = methods[1]; + const param2_1 = method2.getParameters()[0]; + const param2_2 = method2.getParameters()[1]; + expect(param2_1).toBeDefined(); + expect(param2_2).toBeDefined(); + expect(getFQN(param2_1!)).toBe('{SourceFile1.ts}.Interface1.2.method1(string|symbol|Function|GenericType,{strict?:boolean;each?:false;}):TOutput.param1[Parameter]'); + expect(getFQN(param2_2!)).toBe('{SourceFile1.ts}.Interface1.2.method1(string|symbol|Function|GenericType,{strict?:boolean;each?:false;}):TOutput.param2[Parameter]'); + + // Third method parameters + const method3 = methods[2]; + const param3_1 = method3.getParameters()[0]; + const param3_2 = method3.getParameters()[1]; + expect(param3_1).toBeDefined(); + expect(param3_2).toBeDefined(); + expect(getFQN(param3_1!)).toBe('{SourceFile1.ts}.Interface1.3.method1(string|symbol|Function|GenericType,{strict?:boolean;each:true;}):TOutput[].param1[Parameter]'); + expect(getFQN(param3_2!)).toBe('{SourceFile1.ts}.Interface1.3.method1(string|symbol|Function|GenericType,{strict?:boolean;each:true;}):TOutput[].param2[Parameter]'); + }); +}); \ No newline at end of file diff --git a/test/fqn.test.ts b/test/fqn.test.ts index 630e0ffe..635d95a7 100644 --- a/test/fqn.test.ts +++ b/test/fqn.test.ts @@ -68,18 +68,16 @@ describe('getFQN functionality', () => { }); test('should generate unique FQNs for two creations of a class named A within the same source file', () => { - // Find the class declarations via nodes in the AST const classExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.ClassExpression); expect(classExpressions.length).toBe(2); - // find the two classes named A const classA1 = classExpressions.find(c => c.getName() === 'A'); expect(classA1).toBeDefined(); const classA2 = classExpressions.find(c => c.getName() === 'A' && c !== classA1)!; expect(classA2).toBeDefined(); const a1fqn = getFQN(classA1!); - expect(a1fqn).toBe('{sampleFile.ts}.createClassA1.Unnamed_ArrowFunction(7:29).Block(7:35).Unnamed_ClassExpression(8:16)[ClassExpression]'); + expect(a1fqn).toBe('{sampleFile.ts}.createClassA1.createClassA1_Fn.createClassA1_Block.A[ClassExpression]'); const a2fqn = getFQN(classA2!); - expect(a2fqn).toBe('{sampleFile.ts}.createClassA2.Unnamed_ArrowFunction(12:29).Block(12:35).Unnamed_ClassExpression(13:16)[ClassExpression]'); + expect(a2fqn).toBe('{sampleFile.ts}.createClassA2.createClassA2_Fn.createClassA2_Block.A[ClassExpression]'); expect(a1fqn).not.toBe(a2fqn); });