diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 8c977746..22ed3ca1 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -251,7 +251,7 @@ function processVariables(m: ContainerTypes, fmxScope: Famix.ScriptEntity | Fami // Check each VariableDeclaration for object literal methods v.getDeclarations().forEach(varDecl => { const varName = varDecl.getName(); - console.log(`Checking variable: ${varName} at pos=${varDecl.getStart()}`); + // console.log(`Checking variable: ${varName} at pos=${varDecl.getStart()}`); const initializer = varDecl.getInitializer(); if (initializer && Node.isObjectLiteralExpression(initializer)) { initializer.getProperties().forEach(prop => { @@ -259,7 +259,7 @@ function processVariables(m: ContainerTypes, fmxScope: Famix.ScriptEntity | Fami 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()}`); + // console.log(`Found object literal method: ${method.getName()} at pos=${method.getStart()}`); entityDictionary.createOrGetFamixMethod(method, currentCC); }); } @@ -590,7 +590,7 @@ function convertParameterToPropertyRepresentation(param: ParameterDeclaration) { const paramType = param.getType().getText(param); // Determine visibility - let scope: Scope; + let scope: Scope = Scope.Public; if (param.hasModifier(SyntaxKind.PrivateKeyword)) { scope = Scope.Private; } else if (param.hasModifier(SyntaxKind.ProtectedKeyword)) { @@ -598,7 +598,7 @@ function convertParameterToPropertyRepresentation(param: ParameterDeclaration) { } else if (param.hasModifier(SyntaxKind.PublicKeyword)) { scope = Scope.Public; } else { - throw new Error(`Parameter property ${paramName} in constructor does not have a visibility modifier.`); + console.log(`[convertParameterToPropertyRepresentation] Parameter ${paramName} has no visibility modifier, defaulting to public.`); } // Determine if readonly diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index e631762f..b2a1bc40 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -1031,10 +1031,10 @@ export class EntityDictionary { ancestor = this.createOrGetFamixType(typeAncestor.getText(), typeAncestor.getType(), typeAncestor as TSMorphTypeDeclaration); // console.log('Ancestor not found in repo, creating it'); } else { - console.log(`Found ancestor in famixRep: ${ancestor.fullyQualifiedName}`); + // console.log(`Found ancestor in famixRep: ${ancestor.fullyQualifiedName}`); } } else { - console.log(`No type ancestor found for ${typeName} - proceeding without container`); + // console.log(`No type ancestor found for ${typeName} - proceeding without container`); } } @@ -1180,55 +1180,64 @@ export class EntityDictionary { return fmxType; } - /** - * Creates a Famix access - * @param node A node - * @param id An id of a parameter, a variable, a property or an enum member - */ - public createFamixAccess(node: Identifier, id: number): void { - const fmxVar = this.famixRep.getFamixEntityById(id) as Famix.StructuralEntity; - 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.`); - return; // Bail out for now - } else { - logger.debug(`Found accessor to be ${accessor.fullyQualifiedName}.`); - } - - // 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; - } - - // Avoid duplicates - const foundAccess = this.famixRep.getFamixAccessByAccessorAndVariable(accessor, fmxVar); - if (foundAccess) { - logger.debug(`FamixAccess already exists for accessor ${accessor.fullyQualifiedName} and variable ${fmxVar.fullyQualifiedName}.`); +/** + * Creates a Famix access + * @param node A node + * @param id An id of a parameter, a variable, a property or an enum member + */ +public createFamixAccess(node: Identifier, id: number): void { + const fmxVar = this.famixRep.getFamixEntityById(id) as Famix.StructuralEntity; + 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.`); + return; + } else { + logger.debug(`Found accessor to be ${accessor.fullyQualifiedName}.`); + } + + // Check if the accessor is a valid type (method, function, script, module, or class for specific cases) + if (!(accessor instanceof Famix.Method) && + !(accessor instanceof Famix.ArrowFunction) && + !(accessor instanceof Famix.Function) && + !(accessor instanceof Famix.ScriptEntity) && + !(accessor instanceof Famix.Module)) { + // Handle class-level accesses (e.g., decorators, DI) + if (accessor instanceof Famix.Class) { + logger.debug(`Skipping FamixAccess for class accessor ${accessor.fullyQualifiedName} (node: '${node.getText()}', parent: ${node.getParent()?.getKindName() || 'unknown'}). Likely decorator or DI reference.`); return; } - - const fmxAccess = new Famix.Access(); - fmxAccess.accessor = accessor; - fmxAccess.variable = fmxVar; - this.famixRep.addElement(fmxAccess); - this.fmxElementObjectMap.set(fmxAccess, node); - logger.debug(`Created access: ${accessor.fullyQualifiedName} -> ${fmxVar.fullyQualifiedName}`); + logger.error(`Accessor ${accessor.fullyQualifiedName} is not a method, function, etc.`); + return; + } + + // 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); + logger.debug(`Created access: ${accessor.fullyQualifiedName} -> ${fmxVar.fullyQualifiedName}`); +} + /** * Creates a Famix invocation * @param nodeReferringToInvocable A node diff --git a/src/famix_functions/helpers_creation.ts b/src/famix_functions/helpers_creation.ts index ae9276ec..871198b3 100644 --- a/src/famix_functions/helpers_creation.ts +++ b/src/famix_functions/helpers_creation.ts @@ -90,7 +90,7 @@ export function findAncestor(node: Identifier): Node { export function findTypeAncestor(element: Node): Node | undefined { let ancestor: Node | undefined; const ancestors = element.getAncestors(); - console.log(`Ancestors count: ${ancestors.length}`); + // console.log(`Ancestors count: ${ancestors.length}`); ancestor = ancestors.find(a => { const kind = a.getKind(); diff --git a/src/fqn.ts b/src/fqn.ts index 73b71290..2e6d7862 100644 --- a/src/fqn.ts +++ b/src/fqn.ts @@ -100,11 +100,11 @@ function buildStageMethodMap(sourceFile: SourceFile): Map { } /** - * Builds a map of method positions to their index in class/interface/namespace declarations + * Builds a map of method and property 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) + * @returns A Map where keys are node start positions and values are their positional index (1-based) */ -function buildMethodPositionMap(sourceFile: SourceFile): Map { +export function buildMethodPositionMap(sourceFile: SourceFile): Map { const positionMap = new Map(); // console.log(`[buildMethodPositionMap] Starting analysis for file: ${sourceFile.getFilePath()}`); @@ -112,6 +112,23 @@ function buildMethodPositionMap(sourceFile: SourceFile): Map { function processModule(moduleNode: ModuleDeclaration, modulePath: string) { // console.log(`[buildMethodPositionMap] Processing module: ${modulePath}`); + // Track nested modules + const nestedModuleCounts = new Map(); + const nestedModules = moduleNode.getModules(); + nestedModules.forEach(nestedModule => { + if (Node.isModuleDeclaration(nestedModule)) { + const nestedModuleName = nestedModule.getName(); + const count = (nestedModuleCounts.get(nestedModuleName) || 0) + 1; + nestedModuleCounts.set(nestedModuleName, count); + if (count > 1) { + positionMap.set(nestedModule.getStart(), count); + // console.log(`[buildMethodPositionMap] Nested module: ${nestedModuleName}, position: ${nestedModule.getStart()}, index: ${count}`); + } + const newModulePath = `${modulePath}.${nestedModuleName}`; + processModule(nestedModule, newModulePath); + } + }); + // Handle functions directly in the module const functions = moduleNode.getFunctions(); const functionCounts = new Map(); @@ -138,6 +155,18 @@ function buildMethodPositionMap(sourceFile: SourceFile): Map { positionMap.set(method.getStart(), count); // console.log(`[buildMethodPositionMap] Module class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); }); + + // Handle properties within the class + const properties = classNode.getProperties(); + const propertyCounts = new Map(); + + properties.forEach(property => { + const propertyName = property.getName(); + const count = (propertyCounts.get(propertyName) || 0) + 1; + propertyCounts.set(propertyName, count); + positionMap.set(property.getStart(), count); + // console.log(`[buildMethodPositionMap] Module class property: ${propertyName}, position: ${property.getStart()}, index: ${count}`); + }); }); // Handle interfaces within the module @@ -155,27 +184,30 @@ function buildMethodPositionMap(sourceFile: SourceFile): Map { // 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); + const functionExpressions = container.getDescendantsOfKind(SyntaxKind.FunctionExpression); + let funcIndex = 0; + 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()}`); + if (Node.isBlock(parent) || Node.isSourceFile(parent) || Node.isCallExpression(parent)) { + funcIndex++; + positionMap.set(arrow.getStart(), funcIndex); + const { line, column } = sourceFile.getLineAndColumnAtPos(arrow.getStart()); + // console.log(`[buildMethodPositionMap] Arrow function at ${arrow.getStart()} (line: ${line}, col: ${column}), parent: ${parent.getKindName()}, index: ${funcIndex}`); + } + }); + + functionExpressions.forEach(funcExpr => { + const parent = funcExpr.getParent(); + if (Node.isBlock(parent) || Node.isSourceFile(parent) || Node.isCallExpression(parent)) { + funcIndex++; + positionMap.set(funcExpr.getStart(), funcIndex); + const { line, column } = sourceFile.getLineAndColumnAtPos(funcExpr.getStart()); + // console.log(`[buildMethodPositionMap] Function expression at ${funcExpr.getStart()} (line: ${line}, col: ${column}), parent: ${parent.getKindName()}, index: ${funcIndex}`); } }); } @@ -194,8 +226,31 @@ function buildMethodPositionMap(sourceFile: SourceFile): Map { // console.log(`[buildMethodPositionMap] Class method: ${methodName}, position: ${method.getStart()}, index: ${count}`); }); + // Handle properties in top-level classes + const properties = classNode.getProperties(); + const propertyCounts = new Map(); + + properties.forEach(property => { + const propertyName = property.getName(); + const count = (propertyCounts.get(propertyName) || 0) + 1; + propertyCounts.set(propertyName, count); + positionMap.set(property.getStart(), count); + // console.log(`[buildMethodPositionMap] Class property: ${propertyName}, position: ${property.getStart()}, index: ${count}`); + }); + methods.forEach(method => trackArrowFunctions(method)); }); + + // Handle top-level functions + const topLevelFunctionCounts = new Map(); + sourceFile.getFunctions().forEach(func => { + const funcName = func.getName() || `Unnamed_Function(${func.getStart()})`; + const count = (topLevelFunctionCounts.get(funcName) || 0) + 1; + topLevelFunctionCounts.set(funcName, count); + positionMap.set(func.getStart(), count); + // console.log(`[buildMethodPositionMap] Top-level function: ${funcName}, position: ${func.getStart()}, index: ${count}`); + trackArrowFunctions(func); + }); // Handle top-level interfaces sourceFile.getInterfaces().forEach(interfaceNode => { @@ -211,17 +266,25 @@ function buildMethodPositionMap(sourceFile: SourceFile): Map { // console.log(`[buildMethodPositionMap] Interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`); }); methods.forEach(method => trackArrowFunctions(method)); - }); // Handle top-level namespaces/modules + const topLevelModuleCounts = new Map(); sourceFile.getModules().forEach(moduleNode => { if (Node.isModuleDeclaration(moduleNode)) { const moduleName = moduleNode.getName(); + const count = (topLevelModuleCounts.get(moduleName) || 0) + 1; + topLevelModuleCounts.set(moduleName, count); + if (count > 1) { + positionMap.set(moduleNode.getStart(), count); + // console.log(`[buildMethodPositionMap] Top-level module: ${moduleName}, position: ${moduleNode.getStart()}, index: ${count}`); + } processModule(moduleNode, moduleName); } }); + // Handle top-level arrow functions and function expressions + trackArrowFunctions(sourceFile); // console.log(`[buildMethodPositionMap] Final positionMap:`, Array.from(positionMap.entries())); return positionMap; @@ -285,7 +348,6 @@ export function getFQN(node: FQNNode | Node): string { name = currentNode.getName(); } } else { - // if constructor, use "constructor" as name if (Node.isConstructorDeclaration(currentNode)) { name = "constructor"; } else { @@ -308,7 +370,7 @@ export function getFQN(node: FQNNode | Node): string { 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 + return typeText || "any"; }); const returnType = method.getReturnType().getText().replace(/\s+/g, "") || "void"; name = `${name}(${params.join(",")}):${returnType}`; @@ -316,10 +378,13 @@ export function getFQN(node: FQNNode | Node): string { parts.unshift(name); - // Apply positional index for MethodDeclaration, MethodSignature, and FunctionDeclaration - if (Node.isMethodDeclaration(currentNode) || - Node.isMethodSignature(currentNode) || - Node.isFunctionDeclaration(currentNode)) { + // Apply positional index for MethodDeclaration, MethodSignature, FunctionDeclaration, FunctionExpression, and PropertyDeclaration , and ModuleDeclaration + if (Node.isMethodDeclaration(currentNode) || + Node.isMethodSignature(currentNode) || + Node.isFunctionDeclaration(currentNode) || + Node.isFunctionExpression(currentNode) || + Node.isPropertyDeclaration(currentNode) || + Node.isModuleDeclaration(currentNode)) { const key = stageMap.get(currentNode.getStart()); if (key) { parts.unshift(key); @@ -329,8 +394,6 @@ export function getFQN(node: FQNNode | Node): string { 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'}`); } } } @@ -343,7 +406,14 @@ export function getFQN(node: FQNNode | Node): string { Node.isCatchClause(currentNode)) { const name = `${currentNode.getKindName()}(${lc})`; parts.unshift(name); - } + if (Node.isArrowFunction(currentNode)) { + const funcIndex = methodPositionMap.get(currentNode.getStart()); + if (funcIndex && funcIndex > 0) { + parts.unshift(funcIndex.toString()); + // console.log(`[getFQN] Applied funcIndex: ${funcIndex} for ArrowFunction at position ${currentNode.getStart()}`); + } + } + } else if (Node.isTypeParameterDeclaration(currentNode)) { const arrowParent = currentNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction); if (arrowParent) { @@ -353,13 +423,10 @@ export function getFQN(node: FQNNode | Node): string { } } 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(); @@ -370,8 +437,6 @@ export function getFQN(node: FQNNode | Node): string { absolutePathProject ).replace(/\\/g, "/"); - // if (relativePath.includes("..")) { - // } if (relativePath.startsWith("/")) { relativePath = relativePath.slice(1); } diff --git a/test/FunctionAndArrowExpressionFQN.test.ts b/test/FunctionAndArrowExpressionFQN.test.ts new file mode 100644 index 00000000..2176dd5a --- /dev/null +++ b/test/FunctionAndArrowExpressionFQN.test.ts @@ -0,0 +1,159 @@ +import { Project, SyntaxKind, Node } 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: "", + strict: false, + skipLibCheck: true, + }, + useInMemoryFileSystem: true, +}); + +describe('FunctionExpression and ArrowFunction FQN Generation', () => { + let sourceFile: ReturnType; + let importer: Importer; + let fmxRep: any; + + beforeAll(() => { + sourceFile = project.createSourceFile('/pve-stages.ts', ` + const logData = (x: any) => { return x; }; + + module Foo { + const func1 = (d: number) => logData(d); + const func3 = (d: number) => logData(d * 3); + } + + class number1 { + get() { + const arr = [1, 2, 3]; + + arr.forEach(d => logData(d)); + arr.forEach(d => logData(d * 2)); + + arr.forEach(function(d) { + logData(d); + }); + + arr.forEach(function(d) { + logData(d * 2); + }); + + function namedFunc() {} + } + } + `); + + importer = new Importer(); + fmxRep = importer.famixRepFromProject(project); + console.log('All Famix Entities:', Array.from(fmxRep._getAllEntities()).map((e: any) => ({ + fqn: e.fullyQualifiedName, + name: e.name, + type: e.constructor.name + }))); + console.log('Famix ArrowFunctions:', Array.from(fmxRep._getAllEntitiesWithType('ArrowFunction')).map((a: any) => ({ + fqn: a.fullyQualifiedName, + name: a.name, + type: a.constructor.name + }))); + console.log('Famix Functions:', Array.from(fmxRep._getAllEntitiesWithType('Function')).map((f: any) => ({ + fqn: f.fullyQualifiedName, + name: f.name, + type: f.constructor.name + }))); + }); + + it('should parse the source file and generate Famix representation', () => { + expect(fmxRep).toBeTruthy(); + expect(sourceFile).toBeTruthy(); + }); + + it('should generate unique FQNs for top-level duplicate ArrowFunction (Foo module)', () => { + const variableDecls = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration); + const func1 = variableDecls.find(v => v.getName() === 'func1'); + const func3 = variableDecls.find(v => v.getName() === 'func3'); + expect(func1).toBeDefined(); + expect(func3).toBeDefined(); + + // ArrowFunction in func1 + const arrow1 = func1!.getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction); + expect(Node.isArrowFunction(arrow1)).toBe(true); + expect(getFQN(arrow1)).toBe('{pve-stages.ts}.Foo.func1.Unnamed_ArrowFunction(5:31)[ArrowFunction]'); + + // ArrowFunction in func3 + const arrow2 = func3!.getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction); + expect(Node.isArrowFunction(arrow2)).toBe(true); + expect(getFQN(arrow2)).toBe('{pve-stages.ts}.Foo.func3.Unnamed_ArrowFunction(6:31)[ArrowFunction]'); + + // Famix checks + const famixArrowFunctions = fmxRep._getAllEntitiesWithType('ArrowFunction') as Set; + const famixArrow1 = Array.from(famixArrowFunctions).find(a => a.fullyQualifiedName === '{pve-stages.ts}.Foo.func1.Unnamed_ArrowFunction(5:31)[ArrowFunction]'); + expect(famixArrow1).toBeTruthy(); + expect(famixArrow1?.name).toBe('func1'); + + const famixArrow2 = Array.from(famixArrowFunctions).find(a => a.fullyQualifiedName === '{pve-stages.ts}.Foo.func3.Unnamed_ArrowFunction(6:31)[ArrowFunction]'); + expect(famixArrow2).toBeTruthy(); + expect(famixArrow2?.name).toBe('func3'); + }); + + it('should generate unique FQNs for duplicate ArrowFunction and FunctionExpression in number1.get (arr.forEach)', () => { + const method = sourceFile.getClass('number1')?.getMethod('get'); + expect(method).toBeDefined(); + + const forEachCalls = method!.getDescendantsOfKind(SyntaxKind.CallExpression) + .filter(c => c.getExpression().getText() === 'arr.forEach'); + expect(forEachCalls.length).toBe(4); + + // ArrowFunction in first arr.forEach + const arrow1 = forEachCalls[0].getArguments()[0]; + expect(Node.isArrowFunction(arrow1)).toBe(true); + expect(getFQN(arrow1)).toBe('{pve-stages.ts}.number1.get.Block(10:23).Unnamed_ArrowFunction(13:33)[ArrowFunction]'); + + // ArrowFunction in second arr.forEach + const arrow2 = forEachCalls[1].getArguments()[0]; + expect(Node.isArrowFunction(arrow2)).toBe(true); + expect(getFQN(arrow2)).toBe('{pve-stages.ts}.number1.get.Block(10:23).Unnamed_ArrowFunction(14:33)[ArrowFunction]'); + + // FunctionExpression in third arr.forEach + const func1 = forEachCalls[2].getArguments()[0]; + expect(Node.isFunctionExpression(func1)).toBe(true); + expect(getFQN(func1)).toBe('{pve-stages.ts}.number1.get.Block(10:23).3.undefined[FunctionExpression]'); + + // FunctionExpression in fourth arr.forEach + const func2 = forEachCalls[3].getArguments()[0]; + expect(Node.isFunctionExpression(func2)).toBe(true); + expect(getFQN(func2)).toBe('{pve-stages.ts}.number1.get.Block(10:23).4.undefined[FunctionExpression]'); + + // Famix checks + const famixArrowFunctions = fmxRep._getAllEntitiesWithType('ArrowFunction') as Set; + const famixArrow1 = Array.from(famixArrowFunctions).find(a => a.fullyQualifiedName === '{pve-stages.ts}.number1.get.Block(10:23).Unnamed_ArrowFunction(13:33)[ArrowFunction]'); + console.log('famixArrow1:', famixArrow1 ? { fqn: famixArrow1.fullyQualifiedName, name: famixArrow1.name, type: famixArrow1.constructor.name } : 'undefined'); + expect(famixArrow1).toBeTruthy(); + expect(famixArrow1?.name).toBe('(NO_NAME)'); + + const famixArrow2 = Array.from(famixArrowFunctions).find(a => a.fullyQualifiedName === '{pve-stages.ts}.number1.get.Block(10:23).Unnamed_ArrowFunction(13:33)[ArrowFunction]'); + console.log('famixArrow2:', famixArrow2 ? { fqn: famixArrow2.fullyQualifiedName, name: famixArrow2.name, type: famixArrow2.constructor.name } : 'undefined'); + expect(famixArrow2).toBeTruthy(); + expect(famixArrow2?.name).toBe('(NO_NAME)'); + + const famixFunctions = fmxRep._getAllEntitiesWithType('Function') as Set; + const famixFunc1 = Array.from(famixFunctions).find(f => f.fullyQualifiedName === '{pve-stages.ts}.number1.get.Block(10:23).3.undefined[FunctionExpression]'); + expect(famixFunc1).toBeTruthy(); + expect(famixFunc1?.name).toBe('anonymous'); + + const famixFunc2 = Array.from(famixFunctions).find(f => f.fullyQualifiedName === '{pve-stages.ts}.number1.get.Block(10:23).4.undefined[FunctionExpression]'); + expect(famixFunc2).toBeTruthy(); + expect(famixFunc2?.name).toBe('anonymous'); + + // Check namedFunc + const namedFunc = method!.getDescendantsOfKind(SyntaxKind.FunctionDeclaration) + .find(f => f.getName() === 'namedFunc'); + expect(namedFunc).toBeDefined(); + expect(getFQN(namedFunc!)).toBe('{pve-stages.ts}.number1.get.Block(10:23).namedFunc[FunctionDeclaration]'); + const famixNamedFunc = Array.from(famixFunctions).find(f => f.fullyQualifiedName === '{pve-stages.ts}.number1.get.Block(10:23).namedFunc[FunctionDeclaration]'); + expect(famixNamedFunc).toBeTruthy(); + expect(famixNamedFunc?.name).toBe('namedFunc'); + }); +}); \ No newline at end of file diff --git a/test/MethodOverloadFQN.test.ts b/test/MethodOverloadFQN.test.ts index e942c6f9..2146151d 100644 --- a/test/MethodOverloadFQN.test.ts +++ b/test/MethodOverloadFQN.test.ts @@ -10,7 +10,7 @@ const project = new Project({ useInMemoryFileSystem: true, }); -describe('Method and Function Overload with Parameter FQN Generation', () => { +describe('Method, Function Overload, and Class Property FQN Generation', () => { let sourceFile: ReturnType; let importer: Importer; let fmxRep: any; @@ -19,6 +19,8 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { sourceFile = project.createSourceFile('/MethodOverloadFQN.ts', ` declare namespace Namespace2 { class Class1 { + static prop1: string | number; + prop2: boolean; static method1(param1: string): number; static method1(param1: number): number; static method1(param1: any): number; @@ -30,6 +32,7 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { } declare namespace Namespace3 { class Class2 { + prop3: any; static method3(param3: boolean): void; static method3(param3: null): void; } @@ -40,6 +43,8 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { prop2: string; } class Class3 { + prop4: Interface2 | null; + prop5: Class3; static method4(param3: Interface2 | Class3): Class3; static method4(param3: Interface2 | Class3 | undefined): Class3 | undefined; static method4(param3: Interface2 | Class3 | null): Class3 | null; @@ -47,11 +52,13 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { } } declare namespace Namespace1 { - declare module Module1 { + module Module1 { export function function1(param4: Interface3): Interface5; export function function1(param4: Interface3, param6?: Interface6): Interface5; export function function1(param5: Interface7, param6?: Interface6): Interface5; - } + + + } } interface Interface3 {} interface Interface4 {} @@ -62,6 +69,8 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { importer = new Importer(); fmxRep = importer.famixRepFromProject(project); + // Debug: Log all Property entities + console.log('Famix Properties:', Array.from(fmxRep._getAllEntitiesWithType('Property')).map(p => (p as Famix.Property).fullyQualifiedName)); }); it('should parse the source file and generate Famix representation', () => { @@ -69,17 +78,72 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { expect(sourceFile).toBeTruthy(); }); + it('should generate correct FQNs for class properties in Namespace2.Class1', () => { + const properties = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyDeclaration); + + const prop1 = properties.find(p => p.getName() === 'prop1'); + expect(prop1).toBeDefined(); + expect(getFQN(prop1!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.prop1[PropertyDeclaration]'); + + const prop2 = properties.find(p => p.getName() === 'prop2'); + expect(prop2).toBeDefined(); + expect(getFQN(prop2!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.prop2[PropertyDeclaration]'); + + const famixProperties = fmxRep._getAllEntitiesWithType('Property') as Set; + const famixProp1 = Array.from(famixProperties).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.Namespace2.Class1.prop1[PropertyDeclaration]'); + expect(famixProp1).toBeTruthy(); + expect(famixProp1?.name).toBe('prop1'); + + const famixProp2 = Array.from(famixProperties).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.Namespace2.Class1.prop2[PropertyDeclaration]'); + expect(famixProp2).toBeTruthy(); + expect(famixProp2?.name).toBe('prop2'); + }); + + it('should generate correct FQNs for class properties in Namespace3.Class2', () => { + const properties = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyDeclaration); + + const prop3 = properties.find(p => p.getName() === 'prop3'); + expect(prop3).toBeDefined(); + expect(getFQN(prop3!)).toBe('{MethodOverloadFQN.ts}.Namespace3.Class2.prop3[PropertyDeclaration]'); + + const famixProperties = fmxRep._getAllEntitiesWithType('Property') as Set; + const famixProp3 = Array.from(famixProperties).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.Namespace3.Class2.prop3[PropertyDeclaration]'); + expect(famixProp3).toBeTruthy(); + expect(famixProp3?.name).toBe('prop3'); + }); + + it('should generate correct FQNs for class properties in Namespace4.Class3', () => { + const properties = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyDeclaration); + + const prop4 = properties.find(p => p.getName() === 'prop4'); + expect(prop4).toBeDefined(); + expect(getFQN(prop4!)).toBe('{MethodOverloadFQN.ts}.Namespace4.Class3.prop4[PropertyDeclaration]'); + + const prop5 = properties.find(p => p.getName() === 'prop5'); + expect(prop5).toBeDefined(); + expect(getFQN(prop5!)).toBe('{MethodOverloadFQN.ts}.Namespace4.Class3.prop5[PropertyDeclaration]'); + + const famixProperties = fmxRep._getAllEntitiesWithType('Property') as Set; + const famixProp4 = Array.from(famixProperties).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.Namespace4.Class3.prop4[PropertyDeclaration]'); + expect(famixProp4).toBeTruthy(); + expect(famixProp4?.name).toBe('prop4'); + + const famixProp5 = Array.from(famixProperties).find(p => p.fullyQualifiedName === '{MethodOverloadFQN.ts}.Namespace4.Class3.prop5[PropertyDeclaration]'); + expect(famixProp5).toBeTruthy(); + expect(famixProp5?.name).toBe('prop5'); + }); + it('should generate correct FQNs for class methods in namespace Namespace2.Class1', () => { const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); - const method1_1 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText() === 'string'); + const method1_1 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText().includes('string')); expect(method1_1).toBeDefined(); expect(getFQN(method1_1!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.method1[MethodDeclaration]'); - const method1_2 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText() === 'number'); + const method1_2 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText().includes('number')); expect(method1_2).toBeDefined(); expect(getFQN(method1_2!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.2.method1[MethodDeclaration]'); - const method1_3 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText() === 'any'); + const method1_3 = methods.find(m => m.getName() === 'method1' && m.getParameters()[0]?.getType().getText().includes('any')); expect(method1_3).toBeDefined(); expect(getFQN(method1_3!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.3.method1[MethodDeclaration]'); @@ -98,11 +162,11 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { it('should generate correct FQNs for interface methods in namespace Namespace2.Interface1', () => { const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodSignature); - const method2_1 = methods.find(m => m.getName() === 'method2' && m.getParameters()[0]?.getType().getText() === 'string'); + const method2_1 = methods.find(m => m.getName() === 'method2' && m.getParameters()[0]?.getType().getText().includes('string')); expect(method2_1).toBeDefined(); expect(getFQN(method2_1!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Interface1.method2(string):number[MethodSignature]'); - const method2_2 = methods.find(m => m.getName() === 'method2' && m.getParameters()[0]?.getType().getText() === 'number'); + const method2_2 = methods.find(m => m.getName() === 'method2' && m.getParameters()[0]?.getType().getText().includes('number')); expect(method2_2).toBeDefined(); expect(getFQN(method2_2!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Interface1.2.method2(number):number[MethodSignature]'); @@ -117,15 +181,15 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { it('should generate correct FQNs for parameters in Namespace2.Class1.method1', () => { const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); - const param1_1 = parameters.find(p => p.getName() === 'param1' && p.getType().getText() === 'string'); + const param1_1 = parameters.find(p => p.getName() === 'param1' && p.getType().getText().includes('string')); expect(param1_1).toBeDefined(); expect(getFQN(param1_1!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.method1.param1[Parameter]'); - const param1_2 = parameters.find(p => p.getName() === 'param1' && p.getType().getText() === 'number'); + const param1_2 = parameters.find(p => p.getName() === 'param1' && p.getType().getText().includes('number')); expect(param1_2).toBeDefined(); expect(getFQN(param1_2!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.2.method1.param1[Parameter]'); - const param1_3 = parameters.find(p => p.getName() === 'param1' && p.getType().getText() === 'any'); + const param1_3 = parameters.find(p => p.getName() === 'param1' && p.getType().getText().includes('any')); expect(param1_3).toBeDefined(); expect(getFQN(param1_3!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Class1.3.method1.param1[Parameter]'); @@ -145,11 +209,11 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { it('should generate correct FQNs for parameters in Namespace2.Interface1.method2', () => { const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); - const param2_1 = parameters.find(p => p.getName() === 'param2' && p.getType().getText() === 'string'); + const param2_1 = parameters.find(p => p.getName() === 'param2' && p.getType().getText().includes('string')); expect(param2_1).toBeDefined(); expect(getFQN(param2_1!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Interface1.method2(string):number.param2[Parameter]'); - const param2_2 = parameters.find(p => p.getName() === 'param2' && p.getType().getText() === 'number'); + const param2_2 = parameters.find(p => p.getName() === 'param2' && p.getType().getText().includes('number')); expect(param2_2).toBeDefined(); expect(getFQN(param2_2!)).toBe('{MethodOverloadFQN.ts}.Namespace2.Interface1.2.method2(number):number.param2[Parameter]'); @@ -165,11 +229,11 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { it('should generate correct FQNs for class methods in namespace Namespace3.Class2', () => { const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration); - const method3_1 = methods.find(m => m.getName() === 'method3' && m.getParameters()[0]?.getType().getText() === 'boolean'); + const method3_1 = methods.find(m => m.getName() === 'method3' && m.getParameters()[0]?.getType().getText().includes('boolean')); expect(method3_1).toBeDefined(); expect(getFQN(method3_1!)).toBe('{MethodOverloadFQN.ts}.Namespace3.Class2.method3[MethodDeclaration]'); - const method3_2 = methods.find(m => m.getName() === 'method3' && m.getParameters()[0]?.getType().getText() === 'null'); + const method3_2 = methods.find(m => m.getName() === 'method3' && m.getParameters()[0]?.getType().getText().includes('null')); expect(method3_2).toBeDefined(); expect(getFQN(method3_2!)).toBe('{MethodOverloadFQN.ts}.Namespace3.Class2.2.method3[MethodDeclaration]'); @@ -184,11 +248,11 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { it('should generate correct FQNs for parameters in Namespace3.Class2.method3', () => { const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); - const param3_1 = parameters.find(p => p.getName() === 'param3' && p.getType().getText() === 'boolean'); + const param3_1 = parameters.find(p => p.getName() === 'param3' && p.getType().getText().includes('boolean')); expect(param3_1).toBeDefined(); expect(getFQN(param3_1!)).toBe('{MethodOverloadFQN.ts}.Namespace3.Class2.method3.param3[Parameter]'); - const param3_2 = parameters.find(p => p.getName() === 'param3' && p.getType().getText() === 'null'); + const param3_2 = parameters.find(p => p.getName() === 'param3' && p.getType().getText().includes('null')); expect(param3_2).toBeDefined(); expect(getFQN(param3_2!)).toBe('{MethodOverloadFQN.ts}.Namespace3.Class2.2.method3.param3[Parameter]'); @@ -273,33 +337,6 @@ describe('Method and Function Overload with Parameter FQN Generation', () => { expect(famixParam3_4?.name).toBe('param3'); }); - // it('should generate correct FQNs for function overloads in namespace Namespace1.Module1', () => { - // const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration); - // const function1_1 = functions.find(f => f.getName() === 'function1' && f.getParameters()[0]?.getType().getText() === 'Interface3' && f.getParameters().length === 1); - // expect(function1_1).toBeDefined(); - // expect(getFQN(function1_1!)).toBe('{MethodOverloadFQN.ts}.Namespace1.Module1.function1[FunctionDeclaration]'); - - // const function1_2 = functions.find(f => f.getName() === 'function1' && f.getParameters()[0]?.getType().getText() === 'Interface3' && f.getParameters().length === 2); - // expect(function1_2).toBeDefined(); - // expect(getFQN(function1_2!)).toBe('{MethodOverloadFQN.ts}.Namespace1.Module1.2.function1[FunctionDeclaration]'); - - // const function1_3 = functions.find(f => f.getName() === 'function1' && f.getParameters()[0]?.getType().getText() === 'Interface7'); - // expect(function1_3).toBeDefined(); - // expect(getFQN(function1_3!)).toBe('{MethodOverloadFQN.ts}.Namespace1.Module1.3.function1[FunctionDeclaration]'); - - // const famixFunction1_1 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.Namespace1.Module1.function1[FunctionDeclaration]'); - // expect(famixFunction1_1).toBeTruthy(); - // expect(famixFunction1_1.name).toBe('function1'); - - // const famixFunction1_2 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.Namespace1.Module1.2.function1[FunctionDeclaration]'); - // expect(famixFunction1_2).toBeTruthy(); - // expect(famixFunction1_2.name).toBe('function1'); - - // const famixFunction1_3 = fmxRep._getFamixMethod('{MethodOverloadFQN.ts}.Namespace1.Module1.3.function1[FunctionDeclaration]'); - // expect(famixFunction1_3).toBeTruthy(); - // expect(famixFunction1_3.name).toBe('function1'); - // }); - it('should generate correct FQNs for parameters in Namespace1.Module1.function1', () => { const parameters = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); const param4_1 = parameters.find(p => p.getName() === 'param4' && p.getParent().getText().includes('param4: Interface3): Interface5')); diff --git a/test/ModuleDuplicateFQN.test.ts b/test/ModuleDuplicateFQN.test.ts new file mode 100644 index 00000000..5ef60bbb --- /dev/null +++ b/test/ModuleDuplicateFQN.test.ts @@ -0,0 +1,195 @@ +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('Duplicate Module FQN Generation', () => { + let sourceFile: ReturnType; + let importer: Importer; + let fmxRep: any; + + beforeAll(() => { + sourceFile = project.createSourceFile('/ModuleDuplicateFQN.ts', ` + declare namespace Namespace1 { + class Class1 { + prop1: string; + } + } + declare namespace Namespace1 { + class Class2 { + prop2: number; + } + } + declare namespace ParentNamespace { + declare namespace Inner { + function func1(): void; + } + declare namespace Inner { + class Class3 { + prop3: boolean; + } + } + declare namespace Inner { + module SubInner { + function func2(): void; + } + } + } + declare namespace TopLevel { + declare namespace Inner { + class Class4 { + prop4: any; + } + } + } + `); + + importer = new Importer(); + fmxRep = importer.famixRepFromProject(project); + // Debug: Log all Module entities + console.log('Famix Modules:', Array.from(fmxRep._getAllEntitiesWithType('Module')).map((m: any) => m.fullyQualifiedName)); + }); + + it('should parse the source file and generate Famix representation', () => { + expect(fmxRep).toBeTruthy(); + expect(sourceFile).toBeTruthy(); + }); + + it('should generate unique FQNs for duplicate top-level modules (Namespace1)', () => { + const modules = sourceFile.getDescendantsOfKind(SyntaxKind.ModuleDeclaration); + + const namespace1_1 = modules.find(m => m.getName() === 'Namespace1' && m.getClasses().some(c => c.getName() === 'Class1')); + expect(namespace1_1).toBeDefined(); + expect(getFQN(namespace1_1!)).toBe('{ModuleDuplicateFQN.ts}.Namespace1[ModuleDeclaration]'); + + const namespace1_2 = modules.find(m => m.getName() === 'Namespace1' && m.getClasses().some(c => c.getName() === 'Class2')); + expect(namespace1_2).toBeDefined(); + expect(getFQN(namespace1_2!)).toBe('{ModuleDuplicateFQN.ts}.2.Namespace1[ModuleDeclaration]'); + + const famixModules = fmxRep._getAllEntitiesWithType('Module') as Set; + const famixNamespace1_1 = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.Namespace1[ModuleDeclaration]'); + expect(famixNamespace1_1).toBeTruthy(); + expect(famixNamespace1_1?.name).toBe('Namespace1'); + + const famixNamespace1_2 = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.2.Namespace1[ModuleDeclaration]'); + expect(famixNamespace1_2).toBeTruthy(); + expect(famixNamespace1_2?.name).toBe('Namespace1'); + }); + + it('should generate unique FQNs for duplicate nested modules in ParentNamespace (Inner)', () => { + const modules = sourceFile.getDescendantsOfKind(SyntaxKind.ModuleDeclaration); + + const inner1 = modules.find(m => m.getName() === 'Inner' && m.getFunctions().some(f => f.getName() === 'func1')); + expect(inner1).toBeDefined(); + expect(getFQN(inner1!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.Inner[ModuleDeclaration]'); + + const inner2 = modules.find(m => m.getName() === 'Inner' && m.getClasses().some(c => c.getName() === 'Class3')); + expect(inner2).toBeDefined(); + expect(getFQN(inner2!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner[ModuleDeclaration]'); + + const subInner = modules.find(m => m.getName() === 'SubInner' && m.getFunctions().some(f => f.getName() === 'func2')); + expect(subInner).toBeDefined(); + expect(getFQN(subInner!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.3.Inner.SubInner[ModuleDeclaration]'); + + const famixModules = fmxRep._getAllEntitiesWithType('Module') as Set; + const famixInner1 = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.Inner[ModuleDeclaration]'); + expect(famixInner1).toBeTruthy(); + expect(famixInner1?.name).toBe('Inner'); + + const famixInner2 = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner[ModuleDeclaration]'); + expect(famixInner2).toBeTruthy(); + expect(famixInner2?.name).toBe('Inner'); + + const famixSubInner = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.3.Inner.SubInner[ModuleDeclaration]'); + expect(famixSubInner).toBeTruthy(); + expect(famixSubInner?.name).toBe('SubInner'); + }); + + it('should generate correct FQNs for nested module in TopLevel (Inner)', () => { + const modules = sourceFile.getDescendantsOfKind(SyntaxKind.ModuleDeclaration); + + const inner = modules.find(m => m.getName() === 'Inner' && m.getClasses().some(c => c.getName() === 'Class4')); + expect(inner).toBeDefined(); + expect(getFQN(inner!)).toBe('{ModuleDuplicateFQN.ts}.TopLevel.Inner[ModuleDeclaration]'); + + const famixModules = fmxRep._getAllEntitiesWithType('Module') as Set; + const famixInner = Array.from(famixModules).find(m => m.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.TopLevel.Inner[ModuleDeclaration]'); + expect(famixInner).toBeTruthy(); + expect(famixInner?.name).toBe('Inner'); + }); + +it('should generate correct FQNs for entities inside duplicate modules', () => { + const classes = sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration); + const properties = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyDeclaration); + const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration); + + // Class1 in first Namespace1 + const class1 = classes.find(c => c.getName() === 'Class1'); + expect(class1).toBeDefined(); + expect(getFQN(class1!)).toBe('{ModuleDuplicateFQN.ts}.Namespace1.Class1[ClassDeclaration]'); + + const prop1 = properties.find(p => p.getName() === 'prop1'); + expect(prop1).toBeDefined(); + expect(getFQN(prop1!)).toBe('{ModuleDuplicateFQN.ts}.Namespace1.Class1.prop1[PropertyDeclaration]'); + + // Class2 in second Namespace1 + const class2 = classes.find(c => c.getName() === 'Class2'); + expect(class2).toBeDefined(); + expect(getFQN(class2!)).toBe('{ModuleDuplicateFQN.ts}.2.Namespace1.Class2[ClassDeclaration]'); + + const prop2 = properties.find(p => p.getName() === 'prop2'); + expect(prop2).toBeDefined(); + expect(getFQN(prop2!)).toBe('{ModuleDuplicateFQN.ts}.2.Namespace1.Class2.prop2[PropertyDeclaration]'); + + // func1 in ParentNamespace.Inner + const func1 = functions.find(f => f.getName() === 'func1'); + expect(func1).toBeDefined(); + expect(getFQN(func1!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.Inner.func1[FunctionDeclaration]'); + + // Class3 in ParentNamespace.2.Inner + const class3 = classes.find(c => c.getName() === 'Class3'); + expect(class3).toBeDefined(); + expect(getFQN(class3!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner.Class3[ClassDeclaration]'); + + const prop3 = properties.find(p => p.getName() === 'prop3'); + expect(prop3).toBeDefined(); + expect(getFQN(prop3!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner.Class3.prop3[PropertyDeclaration]'); + + // func2 in ParentNamespace.3.Inner.SubInner + const func2 = functions.find(f => f.getName() === 'func2'); + expect(func2).toBeDefined(); + expect(getFQN(func2!)).toBe('{ModuleDuplicateFQN.ts}.ParentNamespace.3.Inner.SubInner.func2[FunctionDeclaration]'); + + // Class4 in TopLevel.Inner + const class4 = classes.find(c => c.getName() === 'Class4'); + expect(class4).toBeDefined(); + expect(getFQN(class4!)).toBe('{ModuleDuplicateFQN.ts}.TopLevel.Inner.Class4[ClassDeclaration]'); + + const prop4 = properties.find(p => p.getName() === 'prop4'); + expect(prop4).toBeDefined(); + expect(getFQN(prop4!)).toBe('{ModuleDuplicateFQN.ts}.TopLevel.Inner.Class4.prop4[PropertyDeclaration]'); + + // Famix entity checks + const famixClasses = fmxRep._getAllEntitiesWithType('Class') as Set; + const famixProperties = fmxRep._getAllEntitiesWithType('Property') as Set; + const famixFunctions = fmxRep._getAllEntitiesWithType('Function') as Set; + + expect(Array.from(famixClasses).find(c => c.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.Namespace1.Class1[ClassDeclaration]')).toBeTruthy(); + expect(Array.from(famixProperties).find(p => p.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.Namespace1.Class1.prop1[PropertyDeclaration]')).toBeTruthy(); + expect(Array.from(famixClasses).find(c => c.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.2.Namespace1.Class2[ClassDeclaration]')).toBeTruthy(); + expect(Array.from(famixProperties).find(p => p.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.2.Namespace1.Class2.prop2[PropertyDeclaration]')).toBeTruthy(); + expect(Array.from(famixFunctions).find(f => f.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.Inner.func1[FunctionDeclaration]')).toBeTruthy(); + expect(Array.from(famixClasses).find(c => c.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner.Class3[ClassDeclaration]')).toBeTruthy(); + expect(Array.from(famixProperties).find(p => p.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.2.Inner.Class3.prop3[PropertyDeclaration]')).toBeTruthy(); + expect(Array.from(famixFunctions).find(f => f.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.ParentNamespace.3.Inner.SubInner.func2[FunctionDeclaration]')).toBeTruthy(); + expect(Array.from(famixClasses).find(c => c.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.TopLevel.Inner.Class4[ClassDeclaration]')).toBeTruthy(); + expect(Array.from(famixProperties).find(p => p.fullyQualifiedName === '{ModuleDuplicateFQN.ts}.TopLevel.Inner.Class4.prop4[PropertyDeclaration]')).toBeTruthy(); + }); +}); \ No newline at end of file