From 475b2ddf8303f9b1e54019b4825ad04aee2a9c13 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:10:00 -0400 Subject: [PATCH 01/25] Add the source anchor deletion implementation --- src/analyze.ts | 12 ++-- src/analyze_functions/process_functions.ts | 2 +- src/famix_functions/EntityDictionary.ts | 51 +++------------ .../famixIndexFileAnchorHelper.ts | 24 +++++++ src/lib/famix/FamixEntitiesTracker.ts | 60 ----------------- src/lib/famix/famix_repository.ts | 65 +++++++++++-------- src/lib/famix/model/famix/inheritance.ts | 4 +- .../associations/inheritance.test.ts | 40 +++++------- .../classes/addClass.test.ts | 4 +- .../classes/changeClass.test.ts | 33 ++++------ .../classes/removeClass.test.ts | 27 +++----- 11 files changed, 120 insertions(+), 202 deletions(-) create mode 100644 src/famix_functions/famixIndexFileAnchorHelper.ts delete mode 100644 src/lib/famix/FamixEntitiesTracker.ts diff --git a/src/analyze.ts b/src/analyze.ts index 8508870d..d9df1bb6 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -6,6 +6,7 @@ import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/Enti import path from "path"; import { TypeScriptToFamixProcessor } from "./analyze_functions/process_functions"; import { getClassesFromSourceFile } from "./famix_functions/helpersTsMorphElementsProcessing"; +import { getFamixIndexFileAnchorFileName } from "./famix_functions/famixIndexFileAnchorHelper"; export const logger = new Logger({ name: "ts2famix", minLevel: 2 }); @@ -81,7 +82,7 @@ export class Importer { const modules = this.processFunctions.modules.getBySourceFileName(fileName); const exports = this.processFunctions.listOfExportMaps.getBySourceFileName(fileName); - this.entityDictionary.setCurrentSourceFileName(fileName); + // this.entityDictionary.setCurrentSourceFileName(fileName); // TODO: check if it is working correctly this.processFunctions.processImportClausesForImportEqualsDeclarations(allExistingSourceFiles, exports); @@ -129,7 +130,7 @@ export class Importer { return this.entityDictionary.famixRep; } - public async updateFamixModelIncrementally(sourceFileChangeMap: Map): Promise { + public updateFamixModelIncrementally(sourceFileChangeMap: Map): void { const allSourceFiles = Array.from(sourceFileChangeMap.values()).flat(); const sourceFilesToCreateEntities = [ ...(sourceFileChangeMap.get(SourceFileChangeType.Create) || []), @@ -138,9 +139,10 @@ export class Importer { allSourceFiles.forEach( file => { - this.entityDictionary.famixRep.removeEntitiesBySourceFile(file.getFilePath()); - // this.entityDictionary.removeEntitiesBySourceFilePath(file.getFilePath()); - // this.processFunctions.removeNodesBySourceFile(file.getFilePath()); + const filePath = getFamixIndexFileAnchorFileName(file.getFilePath(), this.entityDictionary.getAbsolutePath()); + this.entityDictionary.famixRep.removeEntitiesBySourceFile(filePath); + // this.entityDictionary.removeEntitiesBySourceFilePath(filePath); + // this.processFunctions.removeNodesBySourceFile(filePath); } ); diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 414e6e40..45aedbe4 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -52,7 +52,7 @@ export class TypeScriptToFamixProcessor { this.listOfExportMaps.setSourceFileName(sourceFileName); this.processedNodesWithTypeParams.setSourceFileName(sourceFileName); - this.entityDictionary.setCurrentSourceFileName(sourceFileName); + // this.entityDictionary.setCurrentSourceFileName(sourceFileName); } public removeNodesBySourceFile(sourceFile: string) { diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index a70a7ea7..54cfeb1e 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -17,6 +17,8 @@ import * as FQNFunctions from "../fqn"; import path from "path"; import { convertToRelativePath } from "./helpers_path"; import { SourceFileDataMap } from "./SourceFileData"; +import { getFamixIndexFileAnchorFileName } from "./famixIndexFileAnchorHelper"; + export type TSMorphObjectType = ImportDeclaration | ImportEqualsDeclaration | SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | TypeParameterDeclaration | Identifier | Decorator | GetAccessorDeclaration | SetAccessorDeclaration | ImportSpecifier | CommentRange | EnumDeclaration | EnumMember | TypeAliasDeclaration | ExpressionWithTypeArguments | TSMorphParametricType; export type TSMorphTypeDeclaration = TypeAliasDeclaration | PropertyDeclaration | PropertySignature | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | EnumMember | ImportEqualsDeclaration | TSMorphParametricType | TypeParameterDeclaration ; @@ -64,26 +66,6 @@ export class EntityDictionary { return this.absolutePath; } - public setCurrentSourceFileName(name: string): void { - this.fmxAliasMap.setSourceFileName(name); - this.fmxInterfaceMap.setSourceFileName(name); - this.fmxModuleMap.setSourceFileName(name); - this.fmxFileMap.setSourceFileName(name); - this.fmxTypeMap.setSourceFileName(name); - this.fmxPrimitiveTypeMap.setSourceFileName(name); - this.fmxFunctionAndMethodMap.setSourceFileName(name); - this.fmxArrowFunctionMap.setSourceFileName(name); - this.fmxParameterMap.setSourceFileName(name); - this.fmxVariableMap.setSourceFileName(name); - this.fmxImportClauseMap.setSourceFileName(name); - this.fmxEnumMap.setSourceFileName(name); - this.fmxInheritanceMap.setSourceFileName(name); - this.fmxElementObjectMap.setSourceFileName(name); - this.tsMorphElementObjectMap.setSourceFileName(name); - - this.famixRep.famixEntitiesTracker.currentSourceFileToAdd = name; - } - public setAbsolutePath(path: string) { this.absolutePath = path; } @@ -153,7 +135,9 @@ export class EntityDictionary { */ public makeFamixIndexFileAnchor(sourceElement: TSMorphObjectType, famixElement: Famix.SourcedEntity): void { // Famix.Comment is not a named entity (does not have a fullyQualifiedName) - if (!(famixElement instanceof Famix.Comment)) { // must be a named entity + if (!(famixElement instanceof Famix.Comment) + // TODO: consider better approach for the associations, split the NamedEntity class into 2, extend it from inheritance + && !(famixElement instanceof Famix.Inheritance)) { // must be a named entity // insanity check: named entities should have fullyQualifiedName const fullyQualifiedName = (famixElement as Famix.NamedEntity).fullyQualifiedName; if (!fullyQualifiedName || fullyQualifiedName === this.UNKNOWN_VALUE) { @@ -169,27 +153,8 @@ export class EntityDictionary { if (sourceElement !== null) { const absolutePathProject = this.getAbsolutePath(); - const absolutePath = path.normalize(sourceElement.getSourceFile().getFilePath()); - - const positionNodeModules = absolutePath.indexOf('node_modules'); - - let pathInProject: string = ""; - - if (positionNodeModules !== -1) { - const pathFromNodeModules = absolutePath.substring(positionNodeModules); - pathInProject = pathFromNodeModules; - } else { - pathInProject = convertToRelativePath(absolutePath, absolutePathProject); - } - - // revert any backslashes to forward slashes (path.normalize on windows introduces them) - pathInProject = pathInProject.replace(/\\/g, "/"); - - if (pathInProject.startsWith("/")) { - pathInProject = pathInProject.substring(1); - } - - fmxIndexFileAnchor.fileName = pathInProject; + const absolutePath = sourceElement.getSourceFile().getFilePath(); + fmxIndexFileAnchor.fileName = getFamixIndexFileAnchorFileName(absolutePath, absolutePathProject); let sourceStart, sourceEnd // ,sourceLineStart, sourceLineEnd : number; @@ -1393,6 +1358,8 @@ export class EntityDictionary { fmxInheritance.subclass = subClass; fmxInheritance.superclass = superClass; + // TODO: use the correct heritage clause instead of the baseClassOrInterface + this.makeFamixIndexFileAnchor(baseClassOrInterface, fmxInheritance); this.famixRep.addElement(fmxInheritance); // SHOULD THERE BE A SOURCE ANCHOR FOR INHERITANCE? diff --git a/src/famix_functions/famixIndexFileAnchorHelper.ts b/src/famix_functions/famixIndexFileAnchorHelper.ts new file mode 100644 index 00000000..8fde51cd --- /dev/null +++ b/src/famix_functions/famixIndexFileAnchorHelper.ts @@ -0,0 +1,24 @@ +import { convertToRelativePath } from "./helpers_path"; +import path from "path"; + +export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePathProject: string) => { + absolutePath = path.normalize(absolutePath); + const positionNodeModules = absolutePath.indexOf('node_modules'); + + let pathInProject: string = ""; + + if (positionNodeModules !== -1) { + const pathFromNodeModules = absolutePath.substring(positionNodeModules); + pathInProject = pathFromNodeModules; + } else { + pathInProject = convertToRelativePath(absolutePath, absolutePathProject); + } + + // revert any backslashes to forward slashes (path.normalize on windows introduces them) + pathInProject = pathInProject.replace(/\\/g, "/"); + + if (pathInProject.startsWith("/")) { + pathInProject = pathInProject.substring(1); + } + return pathInProject; +}; \ No newline at end of file diff --git a/src/lib/famix/FamixEntitiesTracker.ts b/src/lib/famix/FamixEntitiesTracker.ts deleted file mode 100644 index f49e3b84..00000000 --- a/src/lib/famix/FamixEntitiesTracker.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { FamixBaseElement } from "./famix_base_element"; -import * as Famix from "./model/famix"; - -type SharedEntityType = Famix.PrimitiveType; - -export class FamixEntitiesTracker { - private _currentSourceFileToAdd: string | undefined; - private _entitiesBySourceFile: Map> = new Map(); - private _sharedEntities: Map> = new Map(); - - public set currentSourceFileToAdd(value: string | undefined) { - this._currentSourceFileToAdd = value; - } - - public addEntity(entity: FamixBaseElement): void { - if (!this._currentSourceFileToAdd) { - return; - } - // TODO: Check if only SourcedEntity can be added here - if (!(entity instanceof Famix.SourcedEntity)) { - return; - } - if (this.isEntityShared(entity)) { - const sharedEntity = entity as SharedEntityType; - const sourceFilesWhereUsed = this._sharedEntities.get(sharedEntity) || new Set(); - sourceFilesWhereUsed.add(this._currentSourceFileToAdd); - this._sharedEntities.set(sharedEntity, sourceFilesWhereUsed); - } else { - const entitiesForSourceFile = this._entitiesBySourceFile.get(this._currentSourceFileToAdd) || new Set(); - entitiesForSourceFile.add(entity); - this._entitiesBySourceFile.set(this._currentSourceFileToAdd, entitiesForSourceFile); - } - } - - public getEntitiesBySourceFile(sourceFile: string): Set | undefined { - return this._entitiesBySourceFile.get(sourceFile); - } - - public removeEntitiesBySourceFile(sourceFile: string): Set { - const entitiesToReturn = this._entitiesBySourceFile.get(sourceFile) || new Set(); - this._entitiesBySourceFile.delete(sourceFile); - - for (const [sharedEntity, sourceFiles] of this._sharedEntities.entries()) { - if (sourceFiles.has(sourceFile)) { - sourceFiles.delete(sourceFile); - - if (sourceFiles.size === 0) { - this._sharedEntities.delete(sharedEntity); - entitiesToReturn.add(sharedEntity); - } - } - } - - return entitiesToReturn; - } - - private isEntityShared(entity: FamixBaseElement): boolean { - return entity instanceof Famix.PrimitiveType; - } -} diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index 8ec9ff49..db105908 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -3,7 +3,6 @@ import { Class, Interface, Variable, Method, ArrowFunction, Function as FamixFun import * as Famix from "./model/famix"; import { TSMorphObjectType } from "../../famix_functions/EntityDictionary"; import { logger } from "../../analyze"; -import { FamixEntitiesTracker } from "./FamixEntitiesTracker"; /** * This class is used to store all Famix elements @@ -20,11 +19,6 @@ export class FamixRepository { private famixFiles = new Set(); // All Famix files private idCounter = 1; // Id counter private tsMorphObjectMap = new Map(); // TODO: add this map to have two-way mapping between Famix and TS Morph objects - - private _famixEntitiesTracker: FamixEntitiesTracker = new FamixEntitiesTracker(); - public get famixEntitiesTracker(): FamixEntitiesTracker { - return this._famixEntitiesTracker; - } constructor() { this.addElement(new SourceLanguage()); // add the source language entity (TypeScript) @@ -77,8 +71,19 @@ export class FamixRepository { } } + private getElementsBySourceFile(sourceFile: string): FamixBaseElement[] { + return Array.from(this.elements.values()).filter(e => { + if (e instanceof Famix.SourcedEntity && e.sourceAnchor && e.sourceAnchor instanceof Famix.IndexedFileAnchor) { + return e.sourceAnchor.fileName === sourceFile; + } else if (e instanceof Famix.IndexedFileAnchor) { + return e.fileName === sourceFile; + } + // TODO: check for the SourceAnchor type, maybe make the SourceAnchor abstract cause there is no instance of this class + }); + } + public removeEntitiesBySourceFile(sourceFile: string): FamixBaseElement[] { - const entitiesToRemove = Array.from(this.famixEntitiesTracker.removeEntitiesBySourceFile(sourceFile) || []); + const entitiesToRemove = this.getElementsBySourceFile(sourceFile); this.removeElements(entitiesToRemove); this.removeRelatedAssociations(entitiesToRemove); @@ -106,32 +111,37 @@ export class FamixRepository { this.famixFiles.delete(entity); } - if (entity instanceof Famix.SourcedEntity) { - this.elements.delete(entity.sourceAnchor); - } + // if (entity instanceof Famix.SourcedEntity) { + // this.elements.delete(entity.sourceAnchor); + // } // TODO: maybe delete smth else? } } public removeRelatedAssociations(entities: FamixBaseElement[]): void { for (const entity of entities) { - Array.from(this.elements.values()).forEach(e => { - if (e instanceof Famix.Inheritance && e.subclass === entity) { - this.elements.delete(e); - e.subclass.removeSuperInheritance(e); - e.superclass.removeSubInheritance(e); - } else if (e instanceof Famix.ImportClause && e.importingEntity === entity) { - this.elements.delete(e); - e.importingEntity.removeOutgoingImport(e); - e.importedEntity.removeIncomingImport(e); - } else if (e instanceof Famix.Access && e.accessor === entity) { - this.elements.delete(e); - e.accessor.removeAccess(e); - e.variable.removeIncomingAccess(e); - } else if (e instanceof Famix.Concretisation && e.concreteEntity === entity) { - this.elements.delete(e); - } - }); + // Array.from(this.elements.values()).forEach(e => { + // if (e instanceof Famix.Inheritance && e.subclass === entity) { + // this.elements.delete(e); + // e.subclass.removeSuperInheritance(e); + // e.superclass.removeSubInheritance(e); + // } else if (e instanceof Famix.ImportClause && e.importingEntity === entity) { + // this.elements.delete(e); + // e.importingEntity.removeOutgoingImport(e); + // e.importedEntity.removeIncomingImport(e); + // } else if (e instanceof Famix.Access && e.accessor === entity) { + // this.elements.delete(e); + // e.accessor.removeAccess(e); + // e.variable.removeIncomingAccess(e); + // } else if (e instanceof Famix.Concretisation && e.concreteEntity === entity) { + // this.elements.delete(e); + // } + // }); + + if (entity instanceof Famix.Inheritance) { + entity.subclass.removeSuperInheritance(entity); + entity.superclass.removeSubInheritance(entity); + } // TODO: Add more conditions here for other types of associations } } @@ -299,7 +309,6 @@ export class FamixRepository { element.id = this.idCounter; this.idCounter++; this.validateFQNs(); - this._famixEntitiesTracker.addEntity(element); } /** diff --git a/src/lib/famix/model/famix/inheritance.ts b/src/lib/famix/model/famix/inheritance.ts index 54e90ebe..aa5bb6e4 100644 --- a/src/lib/famix/model/famix/inheritance.ts +++ b/src/lib/famix/model/famix/inheritance.ts @@ -1,9 +1,9 @@ import { FamixJSONExporter } from "../../famix_JSON_exporter"; import { Class } from "./class"; -import { Entity } from "./entity"; import { Interface } from "./interface"; +import { SourcedEntity } from "./sourced_entity"; -export class Inheritance extends Entity { +export class Inheritance extends SourcedEntity { private _superclass!: Class | Interface; private _subclass!: Class | Interface; diff --git a/test/incremental-update/associations/inheritance.test.ts b/test/incremental-update/associations/inheritance.test.ts index dfa91274..b4e5ac34 100644 --- a/test/incremental-update/associations/inheritance.test.ts +++ b/test/incremental-update/associations/inheritance.test.ts @@ -1,39 +1,27 @@ import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; -import { createExpectedFamixModel } from "../incrementalUpdateTestHelper"; +import { createExpectedFamixModel, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; const sourceFileName = 'sourceCode.ts'; const superClassName = 'SuperClass'; const subClassName = 'SubClass'; const superClassCode = ` - class ${superClassName} { - protected property1: string; - protected method1() {} - } + class ${superClassName} { } `; const subClassWithoutInheritanceCode = ` - class ${subClassName} { - method2(): number { - return 42; - } - } + class ${subClassName} { } `; const subClassWithInheritanceCode = ` - class ${subClassName} extends ${superClassName} { - method2(): number { - return 42; - } - } + class ${subClassName} extends ${superClassName} { } `; const superClassChangedCode = ` class ${superClassName} { - protected property1: number; - protected method1Changed() {} + // new comment } `; @@ -65,7 +53,8 @@ describe('Change the inheritance in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInheritance); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithInheritance); @@ -81,7 +70,8 @@ describe('Change the inheritance in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithoutInheritance); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithoutInheritance); @@ -99,7 +89,8 @@ describe('Change the inheritance in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInheritanceChanged); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithInheritanceChanged); @@ -111,7 +102,7 @@ describe('Change the inheritance in a single file', () => { // arrange const sourceCodeWithInheritanceChangedTwice = ` class ${superClassName} { - protected property1: number; + // new comment changed } ${subClassWithInheritanceCode} @@ -123,9 +114,12 @@ describe('Change the inheritance in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInheritanceChanged); // act - importer.updateFamixModelIncrementally([sourceFile]); + let fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInheritanceChangedTwice); - importer.updateFamixModelIncrementally([sourceFile]); + fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithInheritanceChangedTwice); diff --git a/test/incremental-update/classes/addClass.test.ts b/test/incremental-update/classes/addClass.test.ts index 87b36305..be90f97e 100644 --- a/test/incremental-update/classes/addClass.test.ts +++ b/test/incremental-update/classes/addClass.test.ts @@ -17,7 +17,7 @@ describe('Add new classes to a single file', () => { class ${newClassName} { } `; - it('should create new classes in the Famix representation', async () => { + it('should create new classes in the Famix representation', () => { // arrange const testProjectBuilder = new IncrementalUpdateProjectBuilder(); testProjectBuilder.addSourceFile(sourceFileName, sourceCodeWithOneClass); @@ -26,7 +26,7 @@ describe('Add new classes to a single file', () => { // act const fileChangesMap = getUpdateFileChangesMap(sourceFile); - await importer.updateFamixModelIncrementally(fileChangesMap); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithTwoClasses); diff --git a/test/incremental-update/classes/changeClass.test.ts b/test/incremental-update/classes/changeClass.test.ts index 35f77cd7..f0e4f514 100644 --- a/test/incremental-update/classes/changeClass.test.ts +++ b/test/incremental-update/classes/changeClass.test.ts @@ -1,15 +1,12 @@ import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; -import { createExpectedFamixModel, getFqnForClass } from "../incrementalUpdateTestHelper"; +import { createExpectedFamixModel, getFqnForClass, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; const sourceFileName = 'sourceCode.ts'; const existingClassName = 'ExistingClass'; const sourceCodeBefore = ` - class ${existingClassName} { - property1: string; - method1() {} - } + class ${existingClassName} { } `; describe('Modify classes in a single file', () => { @@ -17,11 +14,7 @@ describe('Modify classes in a single file', () => { // arrange const sourceCodeAfter = ` class ${existingClassName} { - property1: string; method1() {} - method2(): number { - return 42; - } } `; @@ -31,7 +24,8 @@ describe('Modify classes in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeAfter); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const existingClass = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName)); @@ -39,7 +33,7 @@ describe('Modify classes in a single file', () => { const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeAfter); expect(existingClass).not.toBeUndefined(); - expect(existingClass!.methods.size).toEqual(2); + expect(existingClass!.methods.size).toEqual(1); expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); @@ -48,8 +42,6 @@ describe('Modify classes in a single file', () => { const sourceCodeAfter = ` class ${existingClassName} { property1: string; - property2: number; - method1() {} } `; @@ -59,7 +51,8 @@ describe('Modify classes in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeAfter); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const existingClass = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName)); @@ -67,7 +60,7 @@ describe('Modify classes in a single file', () => { const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeAfter); expect(existingClass).not.toBeUndefined(); - expect(existingClass!.properties.size).toEqual(2); + expect(existingClass!.properties.size).toEqual(1); expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); @@ -75,10 +68,7 @@ describe('Modify classes in a single file', () => { // arrange const newClassName = 'RenamedExistingClass'; const sourceCodeAfter = ` - class ${newClassName} { - property1: string; - method1() {} - } + class ${newClassName} { } `; const testProjectBuilder = new IncrementalUpdateProjectBuilder(); @@ -87,7 +77,8 @@ describe('Modify classes in a single file', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeAfter); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const oldClass = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName)); @@ -98,8 +89,6 @@ describe('Modify classes in a single file', () => { expect(oldClass).toBeUndefined(); expect(renamedClass).not.toBeUndefined(); expect(renamedClass!.name).toEqual(newClassName); - expect(renamedClass!.properties.size).toEqual(1); - expect(renamedClass!.methods.size).toEqual(1); expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); diff --git a/test/incremental-update/classes/removeClass.test.ts b/test/incremental-update/classes/removeClass.test.ts index a4230139..b2599828 100644 --- a/test/incremental-update/classes/removeClass.test.ts +++ b/test/incremental-update/classes/removeClass.test.ts @@ -1,28 +1,19 @@ import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; -import { createExpectedFamixModel, getFqnForClass } from "../incrementalUpdateTestHelper"; +import { createExpectedFamixModel, getFqnForClass, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; const sourceFileName = 'sourceCode.ts'; const existingClassName1 = 'ExistingClass1'; const existingClassName2 = 'ExistingClass2'; const sourceCodeBefore = ` - class ${existingClassName1} { - property1: string; - method1() {} - } + class ${existingClassName1} { } - class ${existingClassName2} { - property2: number; - method2() {} - } + class ${existingClassName2} { } `; const sourceCodeAfterOneClassDeletion = ` - class ${existingClassName1} { - property1: string; - method1() {} - } + class ${existingClassName1} { } `; const sourceCodeAfterAllClassesDeletion = ` @@ -30,13 +21,14 @@ const sourceCodeAfterAllClassesDeletion = ` `; describe('Delete classes in a single file', () => { - it('should remove one class from the Famix representation', () => { + it('should remove one class from the Famix representation', async () => { const testProjectBuilder = new IncrementalUpdateProjectBuilder(); testProjectBuilder.addSourceFile(sourceFileName, sourceCodeBefore); const { importer, famixRep } = testProjectBuilder.build(); const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeAfterOneClassDeletion); - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); const existingClass = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName1)); const deletedClass = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName2)); @@ -47,13 +39,14 @@ describe('Delete classes in a single file', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); - it('should remove all classes from the Famix representation', () => { + it('should remove all classes from the Famix representation', async () => { const testProjectBuilder = new IncrementalUpdateProjectBuilder(); testProjectBuilder.addSourceFile(sourceFileName, sourceCodeBefore); const { importer, famixRep } = testProjectBuilder.build(); const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeAfterAllClassesDeletion); - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); const deletedClass1 = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName1)); const deletedClass2 = famixRep._getFamixClass(getFqnForClass(sourceFileName, existingClassName2)); From 0a605815800a19002733dfc87d2fe6edaf8a36df Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:54:04 -0400 Subject: [PATCH 02/25] Refactor inheritance and interface creation --- src/analyze.ts | 11 +- src/analyze_functions/process_functions.ts | 135 ++++------ src/famix_functions/EntityDictionary.ts | 250 ++++++------------ .../helpersTsMorphElementsProcessing.ts | 70 ++++- .../associations/inheritance.test.ts | 65 +++++ .../incrementalUpdateExpect.ts | 15 +- test/testUtils.ts | 2 +- 7 files changed, 282 insertions(+), 266 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index d9df1bb6..c38855ce 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -5,7 +5,6 @@ import { Logger } from "tslog"; import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/EntityDictionary"; import path from "path"; import { TypeScriptToFamixProcessor } from "./analyze_functions/process_functions"; -import { getClassesFromSourceFile } from "./famix_functions/helpersTsMorphElementsProcessing"; import { getFamixIndexFileAnchorFileName } from "./famix_functions/famixIndexFileAnchorHelper"; export const logger = new Logger({ name: "ts2famix", minLevel: 2 }); @@ -77,8 +76,7 @@ export class Importer { const accesses = this.processFunctions.accessMap.getBySourceFileName(fileName); const methodsAndFunctionsWithId = this.processFunctions.methodsAndFunctionsWithId.getBySourceFileName(fileName); // const classes = this.processFunctions.classes.getBySourceFileName(fileName); - const classes = getClassesFromSourceFile(sourceFile); - const interfaces = this.processFunctions.interfaces.getBySourceFileName(fileName); + // const interfaces = this.processFunctions.interfaces.getBySourceFileName(fileName); const modules = this.processFunctions.modules.getBySourceFileName(fileName); const exports = this.processFunctions.listOfExportMaps.getBySourceFileName(fileName); @@ -89,8 +87,11 @@ export class Importer { this.processFunctions.processImportClausesForModules(modules, exports); this.processFunctions.processAccesses(accesses); this.processFunctions.processInvocations(methodsAndFunctionsWithId); - this.processFunctions.processInheritances(classes, interfaces, this.processFunctions.interfaces.getAll()); - this.processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId); + // this.processFunctions.processInheritances(classes, interfaces, this.processFunctions.interfaces.getAll()); + + // this.processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId); + //TODO: fix concretisatoion + this.processFunctions.processConcretisations([], [], methodsAndFunctionsWithId); }); } diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 45aedbe4..90badd5e 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -1,4 +1,4 @@ -import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclaration, VariableDeclaration, InterfaceDeclaration, ParameterDeclaration, ConstructorDeclaration, MethodSignature, SourceFile, ModuleDeclaration, PropertyDeclaration, PropertySignature, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ExportedDeclarations, CommentRange, EnumDeclaration, EnumMember, TypeParameterDeclaration, TypeAliasDeclaration, SyntaxKind, FunctionExpression, Block, Identifier, ExpressionWithTypeArguments, ImportDeclaration, Node, ArrowFunction, Scope, ClassExpression } from "ts-morph"; +import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclaration, VariableDeclaration, InterfaceDeclaration, ParameterDeclaration, ConstructorDeclaration, MethodSignature, SourceFile, ModuleDeclaration, PropertyDeclaration, PropertySignature, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ExportedDeclarations, CommentRange, EnumDeclaration, EnumMember, TypeParameterDeclaration, TypeAliasDeclaration, SyntaxKind, FunctionExpression, Block, Identifier, ImportDeclaration, Node, ArrowFunction, Scope, ClassExpression } from "ts-morph"; import * as Famix from "../lib/famix/model/famix"; import { calculate } from "../lib/ts-complex/cyclomatic-service"; import * as fs from 'fs'; @@ -6,7 +6,7 @@ import { logger } from "../analyze"; import { getFQN } from "../fqn"; import { EntityDictionary, InvocableType } from "../famix_functions/EntityDictionary"; import { SourceFileDataArray, SourceFileDataMap, SourceFileDataSet } from "../famix_functions/SourceFileData"; -import { getClassesFromSourceFile } from "../famix_functions/helpersTsMorphElementsProcessing"; +import { getClassesDeclaredInArrowFunctions } from "../famix_functions/helpersTsMorphElementsProcessing"; export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember; export type FamixID = number; @@ -31,7 +31,7 @@ export class TypeScriptToFamixProcessor { public accessMap = new SourceFileDataMap(); // Maps the Famix parameter, variable, property and enum value ids to their ts-morph parameter, variable, property or enum member object // public classes = new SourceFileDataArray(); // Array of all the classes of the source files - public interfaces = new SourceFileDataArray(); // Array of all the interfaces of the source files + // public interfaces = new SourceFileDataArray(); // Array of all the interfaces of the source files public modules = new SourceFileDataArray(); // Array of all the source files which are modules public listOfExportMaps = new SourceFileDataArray>(); // Array of all the export maps private processedNodesWithTypeParams = new SourceFileDataSet(); // Set of nodes that have been processed and have type parameters @@ -47,7 +47,7 @@ export class TypeScriptToFamixProcessor { this.methodsAndFunctionsWithId.setSourceFileName(sourceFileName); this.accessMap.setSourceFileName(sourceFileName); // this.classes.setSourceFileName(sourceFileName); - this.interfaces.setSourceFileName(sourceFileName); + // this.interfaces.setSourceFileName(sourceFileName); this.modules.setSourceFileName(sourceFileName); this.listOfExportMaps.setSourceFileName(sourceFileName); this.processedNodesWithTypeParams.setSourceFileName(sourceFileName); @@ -59,7 +59,7 @@ export class TypeScriptToFamixProcessor { this.methodsAndFunctionsWithId.removeBySourceFileName(sourceFile); this.accessMap.removeBySourceFileName(sourceFile); // this.classes.removeBySourceFileName(sourceFile); - this.interfaces.removeBySourceFileName(sourceFile); + // this.interfaces.removeBySourceFileName(sourceFile); this.modules.removeBySourceFileName(sourceFile); this.listOfExportMaps.removeBySourceFileName(sourceFile); this.processedNodesWithTypeParams.removeBySourceFileName(sourceFile); @@ -86,37 +86,6 @@ export class TypeScriptToFamixProcessor { return path; } - - /** - * Gets the interfaces implemented or extended by a class or an interface - * @param interfaces An array of interfaces - * @param subClass A class or an interface - * @returns An array of InterfaceDeclaration and ExpressionWithTypeArguments containing the interfaces implemented or extended by the subClass - */ - public getImplementedOrExtendedInterfaces(interfaces: Array, subClass: ClassDeclaration | InterfaceDeclaration): Array { - let impOrExtInterfaces: Array; - if (subClass instanceof ClassDeclaration) { - impOrExtInterfaces = subClass.getImplements(); - } - else { - impOrExtInterfaces = subClass.getExtends(); - } - - const interfacesNames = interfaces.map(i => i.getName()); - const implementedOrExtendedInterfaces = new Array(); - - impOrExtInterfaces.forEach(i => { - if (interfacesNames.includes(i.getExpression().getText())) { - implementedOrExtendedInterfaces.push(interfaces[interfacesNames.indexOf(i.getExpression().getText())]); - } - else { - implementedOrExtendedInterfaces.push(i); - } - }); - - return implementedOrExtendedInterfaces; - } - public processFiles(sourceFiles: Array): void { sourceFiles.forEach(file => { logger.info(`File: >>>>>>>>>> ${file.getFilePath()}`); @@ -209,7 +178,8 @@ export class TypeScriptToFamixProcessor { */ private processClasses(m: SourceFile | ModuleDeclaration, fmxScope: Famix.ScriptEntity | Famix.Module): void { logger.debug(`processClasses: ---------- Finding Classes:`); - const classes = getClassesFromSourceFile(m); + const classesInArrowFunctions = getClassesDeclaredInArrowFunctions(m); + const classes = m.getClasses().concat(classesInArrowFunctions); classes.forEach(c => { const fmxClass = this.processClass(c); fmxScope.addType(fmxClass); @@ -358,7 +328,9 @@ export class TypeScriptToFamixProcessor { const fmxAcc = this.processMethod(acc); fmxClass.addMethod(fmxAcc); }); - + + this.processInheritanceForClass(c); + return fmxClass; } @@ -368,16 +340,18 @@ export class TypeScriptToFamixProcessor { * @returns A Famix.Interface or a Famix.ParametricInterface representing the interface */ private processInterface(i: InterfaceDeclaration): Famix.Interface | Famix.ParametricInterface { - this.interfaces.push(i); + // this.interfaces.push(i); - const fmxInterface = this.entityDictionary.createOrGetFamixInterface(i); + const fmxInterface = this.entityDictionary.ensureFamixInterface(i); logger.debug(`Interface: ${i.getName()}, (${i.getType().getText()}), fqn = ${fmxInterface.fullyQualifiedName}`); this.processComments(i, fmxInterface); this.processStructuredType(i, fmxInterface); - + + this.processInheritanceForInterface(i); + return fmxInterface; } @@ -949,53 +923,44 @@ export class TypeScriptToFamixProcessor { }); return importFoundInExports; } - - /** - * Builds a Famix model for the inheritances of the classes and interfaces of the source files - * @param classes An array of classes - * @param interfaces An array of interfaces - */ - public processInheritances(classes: ClassDeclaration[], interfaces: InterfaceDeclaration[], allInterfaces: InterfaceDeclaration[]): void { - logger.info(`Creating inheritances:`); - classes.forEach(cls => { - logger.debug(`Checking class inheritance for ${cls.getName()}`); - const baseClass = cls.getBaseClass(); - if (baseClass !== undefined) { - this.entityDictionary.createOrGetFamixInheritance(cls, baseClass); - logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), extClass: ${baseClass.getName()}, (${baseClass.getType().getText()})`); - } // this is false when the class extends an undefined class - else { - // check for "extends" of unresolved class - const undefinedExtendedClass = cls.getExtends(); - if (undefinedExtendedClass) { - this.entityDictionary.createOrGetFamixInheritance(cls, undefinedExtendedClass); - logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), undefinedExtendedClass: ${undefinedExtendedClass.getText()}`); - } - } - - logger.debug(`Checking interface inheritance for ${cls.getName()}`); - const implementedInterfaces = this.getImplementedOrExtendedInterfaces(allInterfaces, cls); - implementedInterfaces.forEach(implementedIF => { - this.entityDictionary.createOrGetFamixInheritance(cls, implementedIF); - logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), impInter: ${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getName() : implementedIF.getExpression().getText()}, (${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getType().getText() : implementedIF.getExpression().getText()})`); - }); - }); - - interfaces.forEach(interFace => { - try { - logger.debug(`Checking interface inheritance for ${interFace.getName()}`); - const extendedInterfaces = this.getImplementedOrExtendedInterfaces(allInterfaces, interFace); - extendedInterfaces.forEach(extendedInterface => { - this.entityDictionary.createOrGetFamixInheritance(interFace, extendedInterface); - - logger.debug(`interFace: ${interFace.getName()}, (${interFace.getType().getText()}), extendedInterface: ${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getName() : extendedInterface.getExpression().getText()}, (${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getType().getText() : extendedInterface.getExpression().getText()})`); - }); - } - catch (error) { - logger.error(`> WARNING: got exception ${error}. Continuing...`); + + private processInheritanceForClass(cls: ClassDeclaration) { + logger.debug(`Checking class inheritance for ${cls.getName()}`); + const baseClass = cls.getBaseClass(); + if (baseClass !== undefined) { + this.entityDictionary.ensureFamixClassToClassInheritance(cls, baseClass); + logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), extClass: ${baseClass.getName()}, (${baseClass.getType().getText()})`); + } // this is false when the class extends an undefined class + else { + // check for "extends" of unresolved class + const undefinedExtendedClass = cls.getExtends(); + if (undefinedExtendedClass) { + this.entityDictionary.ensureFamixClassToClassInheritance(cls, undefinedExtendedClass); + logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), undefinedExtendedClass: ${undefinedExtendedClass.getText()}`); } + } + + logger.debug(`Checking interface inheritance for ${cls.getName()}`); + + cls.getImplements().forEach(implementedIF => { + this.entityDictionary.ensureFamixInterfaceInheritance(cls, implementedIF); + logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), impInter: ${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getName() : implementedIF.getExpression().getText()}, (${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getType().getText() : implementedIF.getExpression().getText()})`); }); } + + private processInheritanceForInterface(interFace: InterfaceDeclaration) { + try { + logger.debug(`Checking interface inheritance for ${interFace.getName()}`); + + interFace.getExtends().forEach(extendedInterface => { + this.entityDictionary.ensureFamixInterfaceInheritance(interFace, extendedInterface); + logger.debug(`interFace: ${interFace.getName()}, (${interFace.getType().getText()}), extendedInterface: ${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getName() : extendedInterface.getExpression().getText()}, (${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getType().getText() : extendedInterface.getExpression().getText()})`); + }); + } + catch (error) { + logger.error(`> WARNING: got exception ${error}. Continuing...`); + } + } /** * Builds a Famix model for the invocations of the methods and functions of the source files diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index 54cfeb1e..b4e9701d 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -5,7 +5,7 @@ */ -import { ClassDeclaration, ConstructorDeclaration, FunctionDeclaration, Identifier, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, PropertyDeclaration, PropertySignature, SourceFile, TypeParameterDeclaration, VariableDeclaration, ParameterDeclaration, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ImportSpecifier, CommentRange, EnumDeclaration, EnumMember, TypeAliasDeclaration, FunctionExpression, ImportDeclaration, ImportEqualsDeclaration, SyntaxKind, Expression, TypeNode, Scope, ArrowFunction, ExpressionWithTypeArguments, HeritageClause, ts, Type, Node } from "ts-morph"; +import { ClassDeclaration, ConstructorDeclaration, FunctionDeclaration, Identifier, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, PropertyDeclaration, PropertySignature, SourceFile, TypeParameterDeclaration, VariableDeclaration, ParameterDeclaration, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ImportSpecifier, CommentRange, EnumDeclaration, EnumMember, TypeAliasDeclaration, FunctionExpression, ImportDeclaration, ImportEqualsDeclaration, SyntaxKind, Expression, TypeNode, Scope, ArrowFunction, ExpressionWithTypeArguments, ts, Type, Node } from "ts-morph"; import { isAmbient, isNamespace } from "../analyze_functions/process_functions"; import * as Famix from "../lib/famix/model/famix"; import { FamixRepository } from "../lib/famix/famix_repository"; @@ -41,7 +41,7 @@ export class EntityDictionary { public famixRep = new FamixRepository(); private fmxAliasMap = new SourceFileDataMap(); // Maps the alias names to their Famix model // private fmxClassMap = new SourceFileDataMap(); // Maps the fully qualified class names to their Famix model - private fmxInterfaceMap = new SourceFileDataMap(); // Maps the interface names to their Famix model + // private fmxInterfaceMap = new SourceFileDataMap(); // Maps the interface names to their Famix model private fmxModuleMap = new SourceFileDataMap(); // Maps the namespace names to their Famix model private fmxFileMap = new SourceFileDataMap(); // Maps the source file names to their Famix model private fmxTypeMap = new SourceFileDataMap(); // Maps the types declarations to their Famix model @@ -338,7 +338,6 @@ export class EntityDictionary { } fmxClass.name = clsName; - this.initFQN(cls, fmxClass); fmxClass.isAbstract = isAbstract; return fmxClass; }; @@ -360,6 +359,7 @@ export class EntityDictionary { } const fmxNewElement = mapToFamixElementFn(node); + this.initFQN(node as unknown as TSMorphObjectType, fmxNewElement); this.makeFamixIndexFileAnchor(node as unknown as TSMorphObjectType, fmxNewElement); this.famixRep.addElement(fmxNewElement); @@ -372,13 +372,10 @@ export class EntityDictionary { * @param inter An interface * @returns The Famix model of the interface */ - public createOrGetFamixInterface(inter: InterfaceDeclaration): Famix.Interface | Famix.ParametricInterface { - - let fmxInterface: Famix.Interface | Famix.ParametricInterface; - const interName = inter.getName(); - const interFullyQualifiedName = FQNFunctions.getFQN(inter, this.getAbsolutePath()); - const foundInterface = this.fmxInterfaceMap.get(interFullyQualifiedName); - if (!foundInterface) { + public ensureFamixInterface(inter: InterfaceDeclaration): Famix.Interface | Famix.ParametricInterface { + const mapToFamixElement = (inter: InterfaceDeclaration) => { + let fmxInterface: Famix.Interface | Famix.ParametricInterface; + const isGeneric = inter.getTypeParameters().length; if (isGeneric) { fmxInterface = new Famix.ParametricInterface(); @@ -386,21 +383,14 @@ export class EntityDictionary { else { fmxInterface = new Famix.Interface(); } + fmxInterface.name = inter.getName(); - fmxInterface.name = interName; - this.initFQN(inter, fmxInterface); - this.makeFamixIndexFileAnchor(inter, fmxInterface); - - this.fmxInterfaceMap.set(interFullyQualifiedName, fmxInterface); - - this.famixRep.addElement(fmxInterface); + return fmxInterface; + }; - this.fmxElementObjectMap.set(fmxInterface,inter); - } - else { - fmxInterface = foundInterface; - } - return fmxInterface; + return this.ensureFamixElement( + inter, mapToFamixElement + ); } @@ -428,7 +418,8 @@ export class EntityDictionary { let concElement: ParametricVariantType | undefined; - if (!this.fmxInterfaceMap.has(fullyQualifiedFilename) && + if ( + // !this.fmxInterfaceMap.has(fullyQualifiedFilename) && // !this.fmxClassMap.has(fullyQualifiedFilename) && !this.fmxFunctionAndMethodMap.has(fullyQualifiedFilename)){ concElement = _.cloneDeep(concreteElement); @@ -449,7 +440,7 @@ export class EntityDictionary { if (concreteElement instanceof Famix.ParametricClass) { // this.fmxClassMap.set(fullyQualifiedFilename, concElement as Famix.ParametricClass); } else if (concreteElement instanceof Famix.ParametricInterface) { - this.fmxInterfaceMap.set(fullyQualifiedFilename, concElement as Famix.ParametricInterface); + // this.fmxInterfaceMap.set(fullyQualifiedFilename, concElement as Famix.ParametricInterface); } else if (concreteElement instanceof Famix.ParametricFunction) { this.fmxFunctionAndMethodMap.set(fullyQualifiedFilename, concElement as Famix.ParametricFunction); } else { // if (concreteElement instanceof Famix.ParametricMethod) { @@ -461,7 +452,7 @@ export class EntityDictionary { if (concreteElement instanceof Famix.ParametricClass) { // concElement = this.fmxClassMap.get(fullyQualifiedFilename) as Famix.ParametricClass; } else if (concreteElement instanceof Famix.ParametricInterface) { - concElement = this.fmxInterfaceMap.get(fullyQualifiedFilename) as Famix.ParametricInterface; + // concElement = this.fmxInterfaceMap.get(fullyQualifiedFilename) as Famix.ParametricInterface; } else if (concreteElement instanceof Famix.ParametricFunction) { concElement = this.fmxFunctionAndMethodMap.get(fullyQualifiedFilename) as Famix.ParametricFunction; } else { // if (concreteElement instanceof Famix.ParametricMethod) { @@ -1282,94 +1273,77 @@ export class EntityDictionary { this.fmxElementObjectMap.set(fmxInvocation,nodeReferringToInvocable); } - /** - * Creates a Famix inheritance - * @param baseClassOrInterface A class or an interface (subclass) - * @param inheritedClassOrInterface The inherited class or interface (superclass) - */ - public createOrGetFamixInheritance(baseClassOrInterface: ClassDeclaration | InterfaceDeclaration, inheritedClassOrInterface: ClassDeclaration | InterfaceDeclaration | ExpressionWithTypeArguments): void { - logger.debug(`Creating FamixInheritance for ${baseClassOrInterface.getText()} and ${inheritedClassOrInterface.getText()} [${inheritedClassOrInterface.constructor.name}].`); - const fmxInheritance = new Famix.Inheritance(); + public ensureFamixClassToClassInheritance( + subClass: ClassDeclaration, superClass: ClassDeclaration | ExpressionWithTypeArguments + ) { + const subClassFamix = this.ensureFamixClass(subClass); + let superClassFamix: Famix.Class | undefined; - let subClass: Famix.Class | Famix.Interface | undefined; - if (baseClassOrInterface instanceof ClassDeclaration) { - subClass = this.ensureFamixClass(baseClassOrInterface); - } else { - subClass = this.createOrGetFamixInterface(baseClassOrInterface); - } + // Case 1: class extends class + if (superClass instanceof ClassDeclaration) { + superClassFamix = this.ensureFamixClass(superClass); - if (!subClass) { - throw new Error(`Subclass ${baseClassOrInterface} not found in Class or Interface maps.`); + // Case 2: class extends undefined class + } else { + const classDeclaration = getInterfaceOrClassDeclarationFromExpression(superClass) as ClassDeclaration | undefined; + if (classDeclaration) { + superClassFamix = this.ensureFamixClass(classDeclaration); + } else { + logger.error(`Class declaration not found for ${superClass.getText()}.`); + superClassFamix = this.createOrGetFamixClassStub(superClass); + } } - let superClass: Famix.Class | Famix.Interface | undefined; - - if (inheritedClassOrInterface instanceof ClassDeclaration) { - superClass = this.ensureFamixClass(inheritedClassOrInterface); - } else if (inheritedClassOrInterface instanceof InterfaceDeclaration) { - superClass = this.createOrGetFamixInterface(inheritedClassOrInterface); - } else { - // inheritedClassOrInterface instanceof ExpressionWithTypeArguments - // must determine if inheritedClassOrInterface is a class or an interface - // then find the declaration, else it's a stub - - const heritageClause = inheritedClassOrInterface.getParent(); - if (heritageClause instanceof HeritageClause) { - // cases: 1) class extends class, 2) class implements interface, 3) interface extends interface - - // class extends class - if (heritageClause.getText().startsWith("extends") && baseClassOrInterface instanceof ClassDeclaration) { - const classDeclaration = getInterfaceOrClassDeclarationFromExpression(inheritedClassOrInterface); - if (classDeclaration !== undefined && classDeclaration instanceof ClassDeclaration) { - superClass = this.ensureFamixClass(classDeclaration); - } else { - logger.error(`Class declaration not found for ${inheritedClassOrInterface.getText()}.`); - superClass = this.createOrGetFamixClassStub(inheritedClassOrInterface); - } - } - else if (heritageClause.getText().startsWith("implements") && baseClassOrInterface instanceof ClassDeclaration // class implements interface - || (heritageClause.getText().startsWith("extends") && baseClassOrInterface instanceof InterfaceDeclaration)) { // interface extends interface + logger.debug(`Creating FamixInheritance for ${subClass.getText()} and ${superClass.getText()} [${superClass.constructor.name}].`); + this.createFamixInheritance(subClassFamix, superClassFamix, subClass); + } - const interfaceOrClassDeclaration = getInterfaceOrClassDeclarationFromExpression(inheritedClassOrInterface); - if (interfaceOrClassDeclaration !== undefined && interfaceOrClassDeclaration instanceof InterfaceDeclaration) { - superClass = this.createOrGetFamixInterface(interfaceOrClassDeclaration); - } else { - logger.error(`Interface declaration not found for ${inheritedClassOrInterface.getText()}.`); - superClass = this.createOrGetFamixInterfaceStub(inheritedClassOrInterface); - } - } else { - // throw new Error(`Parent of ${inheritedClassOrInterface.getText()} is not a class or an interface.`); - logger.error(`Parent of ${inheritedClassOrInterface.getText()} is not a class or an interface.`); - superClass = this.createOrGetFamixInterfaceStub(inheritedClassOrInterface); - } + public ensureFamixInterfaceInheritance( + subClassOrInterface: ClassDeclaration | InterfaceDeclaration, superInterface: InterfaceDeclaration | ExpressionWithTypeArguments + ) { + const getSubFamixElement = () => { + if (subClassOrInterface instanceof ClassDeclaration) { + return this.ensureFamixClass(subClassOrInterface); } else { - throw new Error(`Heritage clause not found for ${inheritedClassOrInterface.getText()}.`); + return this.ensureFamixInterface(subClassOrInterface); } + }; + const subClassOrInterfaceFamix = getSubFamixElement(); - } - - // // WHY DO WE NEED THESE LINES? WE HAVE ALREADY ADDED THIS CLASS TO THE FAMIX REPOSITORY - // // IS IT CONNECTED WITH USING createOrGetFamixInterfaceStub? - // this.fmxElementObjectMap.set(superClass, inheritedClassOrInterface); + let superInterfaceFamix: Famix.Interface | undefined; - // this.makeFamixIndexFileAnchor(inheritedClassOrInterface, superClass); + // Case 1: class implements interface // Case 1.1: interface extends interface + if (superInterface instanceof InterfaceDeclaration) { + superInterfaceFamix = this.ensureFamixInterface(superInterface); + // Case 2: class implements undefined interface // Case 2.1: interface extends undefined interface + } else { + const interfaceDeclaration = getInterfaceOrClassDeclarationFromExpression(superInterface) as InterfaceDeclaration | undefined; + if (interfaceDeclaration) { + superInterfaceFamix = this.ensureFamixInterface(interfaceDeclaration); + } else { + logger.error(`Interface declaration not found for ${superInterface.getText()}.`); + superInterfaceFamix = this.createOrGetFamixInterfaceStub(superInterface); + } + } - // this.famixRep.addElement(superClass); + logger.debug(`Creating FamixInheritance for ${subClassOrInterface.getText()} and ${superInterface.getText()} [${superInterface.constructor.name}].`); + this.createFamixInheritance(subClassOrInterfaceFamix, superInterfaceFamix, subClassOrInterface); + } - fmxInheritance.subclass = subClass; - fmxInheritance.superclass = superClass; + private createFamixInheritance( + subClassFamix: Famix.Class | Famix.Interface, + superClassFamix: Famix.Class | Famix.Interface, + subClass: ClassDeclaration | InterfaceDeclaration | ExpressionWithTypeArguments, + ) { + const fmxInheritance = new Famix.Inheritance(); + fmxInheritance.subclass = subClassFamix; + fmxInheritance.superclass = superClassFamix; // TODO: use the correct heritage clause instead of the baseClassOrInterface - this.makeFamixIndexFileAnchor(baseClassOrInterface, fmxInheritance); - + this.makeFamixIndexFileAnchor(subClass, fmxInheritance); this.famixRep.addElement(fmxInheritance); - // SHOULD THERE BE A SOURCE ANCHOR FOR INHERITANCE? - - // no FQN for inheritance - - // We don't map inheritance to the source code element because there are two elements (super, sub) - // this.fmxElementObjectMap.set(fmxInheritance, null); - } + + // TODO: refactor to use the ensureFamixElement method createOrGetFamixClassStub(unresolvedInheritedClass: ExpressionWithTypeArguments): Famix.Class { // make a FQN for the stub const fqn = FQNFunctions.getFQNUnresolvedInheritedClassOrInterface(unresolvedInheritedClass); @@ -1383,11 +1357,12 @@ export class EntityDictionary { stub.isStub = true; stub.fullyQualifiedName = fqn; this.famixRep.addElement(stub); - this.fmxElementObjectMap.set(stub, unresolvedInheritedClass); + this.makeFamixIndexFileAnchor(unresolvedInheritedClass, stub); return stub; } } + // TODO: refactor to use the ensureFamixElement method createOrGetFamixInterfaceStub(unresolvedInheritedInterface: ExpressionWithTypeArguments): Famix.Interface { // make a FQN for the stub const fqn = FQNFunctions.getFQNUnresolvedInheritedClassOrInterface(unresolvedInheritedInterface); @@ -1401,7 +1376,7 @@ export class EntityDictionary { stub.isStub = true; stub.fullyQualifiedName = fqn; this.famixRep.addElement(stub); - this.fmxElementObjectMap.set(stub, unresolvedInheritedInterface); + this.makeFamixIndexFileAnchor(unresolvedInheritedInterface, stub); return stub; } } @@ -1688,7 +1663,7 @@ export class EntityDictionary { genEntity = this.ensureFamixClass(EntityDeclaration) as Famix.ParametricClass; } else { EntityDeclaration = entity.getExpression().getSymbol()?.getDeclarations()[0] as InterfaceDeclaration; - genEntity = this.createOrGetFamixInterface(EntityDeclaration) as Famix.ParametricInterface; + genEntity = this.ensureFamixInterface(EntityDeclaration) as Famix.ParametricInterface; } const genParams = EntityDeclaration.getTypeParameters().map((param) => param.getText()); const args = element.getHeritageClauses()[0].getTypeNodes()[0].getTypeArguments(); @@ -1821,7 +1796,7 @@ export class EntityDictionary { const conParams = cls.getHeritageClauses()[0].getTypeNodes()[0].getTypeArguments().map((param) => param.getText()); const args = cls.getHeritageClauses()[0].getTypeNodes()[0].getTypeArguments(); if (!Helpers.arraysAreEqual(conParams,genParams)) { - const genInterface = this.createOrGetFamixInterface(interfaceDeclaration) as Famix.ParametricInterface; + const genInterface = this.ensureFamixInterface(interfaceDeclaration) as Famix.ParametricInterface; const conInterface = this.createOrGetFamixConcreteElement(genInterface,interfaceDeclaration,args); const concretisations = this.famixRep._getAllEntitiesWithType("Concretisation") as Set; let createConcretisation : boolean = true; @@ -1867,7 +1842,7 @@ export class EntityDictionary { if (element instanceof ClassDeclaration) { genElement = this.ensureFamixClass(element) as Famix.ParametricClass; } else { - genElement = this.createOrGetFamixInterface(element) as Famix.ParametricInterface; + genElement = this.ensureFamixInterface(element) as Famix.ParametricInterface; } const concElement = this.createOrGetFamixConcreteElement(genElement, element, args); const concretisations = this.famixRep._getAllEntitiesWithType("Concretisation") as Set; @@ -1910,7 +1885,6 @@ export class EntityDictionary { public removeEntitiesBySourceFilePath(sourceFilePath: string) { this.fmxAliasMap.removeBySourceFileName(sourceFilePath); - this.fmxInterfaceMap.removeBySourceFileName(sourceFilePath); this.fmxModuleMap.removeBySourceFileName(sourceFilePath); this.fmxFileMap.removeBySourceFileName(sourceFilePath); this.fmxTypeMap.removeBySourceFileName(sourceFilePath); @@ -1970,70 +1944,10 @@ function isTypeContext(sourceElement: TSMorphObjectType): boolean { return typeContextKinds.has(sourceElement.getKind()); } -function getInterfaceOrClassDeclarationFromExpression(expression: ExpressionWithTypeArguments): InterfaceDeclaration | ClassDeclaration | undefined { - // Step 1: Get the type of the expression - const type = expression.getType(); - - // Step 2: Get the symbol associated with the type - let symbol = type.getSymbol(); - - if (!symbol) { - // If symbol is not found, try to get the symbol from the identifier - const identifier = expression.getFirstDescendantByKind(SyntaxKind.Identifier); - if (!identifier) { - throw new Error(`Identifier not found for ${expression.getText()}.`); - } - symbol = identifier.getSymbol(); - if (!symbol) { - throw new Error(`Symbol not found for ${identifier.getText()}.`); - } - } - - // Step 3: Resolve the symbol to find the actual declaration - const interfaceDeclaration = resolveSymbolToInterfaceOrClassDeclaration(symbol); - - if (!interfaceDeclaration) { - logger.error(`Interface declaration not found for ${expression.getText()}.`); - } - - return interfaceDeclaration; -} -import { Symbol as TSMorphSymbol, Node as TsMorphNode } from "ts-morph"; +import { Node as TsMorphNode } from "ts-morph"; import _ from "lodash"; - -function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): InterfaceDeclaration | ClassDeclaration | undefined { - // Get the declarations associated with the symbol - const declarations = symbol.getDeclarations(); - - // Filter for InterfaceDeclaration or ClassDeclaration - const interfaceOrClassDeclaration = declarations.find( - declaration => - declaration instanceof InterfaceDeclaration || - declaration instanceof ClassDeclaration) as InterfaceDeclaration | ClassDeclaration | undefined; - - if (interfaceOrClassDeclaration) { - return interfaceOrClassDeclaration; - } - - // Handle imports: If the symbol is imported, resolve the import to find the actual declaration - for (const declaration of declarations) { - if (declaration.getKind() === SyntaxKind.ImportSpecifier) { - const importSpecifier = declaration as ImportSpecifier; - const importDeclaration = importSpecifier.getImportDeclaration(); - const moduleSpecifier = importDeclaration.getModuleSpecifierSourceFile(); - - if (moduleSpecifier) { - const exportedSymbols = moduleSpecifier.getExportSymbols(); - const exportedSymbol = exportedSymbols.find(symbol => symbol.getName() === importSpecifier.getName()); - if (exportedSymbol) { - return resolveSymbolToInterfaceOrClassDeclaration(exportedSymbol); - } - } - } - } - return undefined; -} +import { getInterfaceOrClassDeclarationFromExpression } from "./helpersTsMorphElementsProcessing"; export function getPrimitiveTypeName(type: Type): string | undefined { diff --git a/src/famix_functions/helpersTsMorphElementsProcessing.ts b/src/famix_functions/helpersTsMorphElementsProcessing.ts index de499aff..2745ac43 100644 --- a/src/famix_functions/helpersTsMorphElementsProcessing.ts +++ b/src/famix_functions/helpersTsMorphElementsProcessing.ts @@ -1,4 +1,5 @@ -import { ArrowFunction, ClassDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind } from "ts-morph"; +import { ArrowFunction, ClassDeclaration, ExpressionWithTypeArguments, ImportSpecifier, InterfaceDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind } from "ts-morph"; +import { Symbol as TSMorphSymbol } from "ts-morph"; /** * ts-morph doesn't find classes in arrow functions, so we need to find them manually @@ -26,8 +27,67 @@ export function getArrowFunctionClasses(f: ArrowFunction): ClassDeclaration[] { return classes; } -export function getClassesFromSourceFile(sourceFile: SourceFile | ModuleDeclaration) { - const classesInArrowFunctions = getClassesDeclaredInArrowFunctions(sourceFile); - const classes = sourceFile.getClasses().concat(classesInArrowFunctions); - return classes; +// NOTE: Finding the symbol may not work when used bare import without baseUrl +// e.g. import { MyInterface } from "outsideInterface"; will not work if baseUrl is not set +export function getInterfaceOrClassDeclarationFromExpression(expression: ExpressionWithTypeArguments): InterfaceDeclaration | ClassDeclaration | undefined { + // Step 1: Get the type of the expression + const type = expression.getType(); + + // Step 2: Get the symbol associated with the type + let symbol = type.getSymbol(); + + if (!symbol) { + // If symbol is not found, try to get the symbol from the identifier + const identifier = expression.getFirstDescendantByKind(SyntaxKind.Identifier); + if (!identifier) { + throw new Error(`Identifier not found for ${expression.getText()}.`); + } + symbol = identifier.getSymbol(); + if (!symbol) { + throw new Error(`Symbol not found for ${identifier.getText()}.`); + } + } + + // Step 3: Resolve the symbol to find the actual declaration + const interfaceDeclaration = resolveSymbolToInterfaceOrClassDeclaration(symbol); + + if (!interfaceDeclaration) { + // logger.error(`Interface declaration not found for ${expression.getText()}.`); + } + + return interfaceDeclaration; +} + +function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): InterfaceDeclaration | ClassDeclaration | undefined { + // Get the declarations associated with the symbol + const declarations = symbol.getDeclarations(); + + // Filter for InterfaceDeclaration or ClassDeclaration + const interfaceOrClassDeclaration = declarations.find( + declaration => + declaration instanceof InterfaceDeclaration || + declaration instanceof ClassDeclaration) as InterfaceDeclaration | ClassDeclaration | undefined; + + if (interfaceOrClassDeclaration) { + return interfaceOrClassDeclaration; + } + + // Handle imports: If the symbol is imported, resolve the import to find the actual declaration + for (const declaration of declarations) { + if (declaration.getKind() === SyntaxKind.ImportSpecifier) { + const importSpecifier = declaration as ImportSpecifier; + const importDeclaration = importSpecifier.getImportDeclaration(); + const moduleSpecifier = importDeclaration.getModuleSpecifierSourceFile(); + // importDeclaration. + + if (moduleSpecifier) { + const exportedSymbols = moduleSpecifier.getExportSymbols(); + const exportedSymbol = exportedSymbols.find(symbol => symbol.getName() === importSpecifier.getName()); + if (exportedSymbol) { + return resolveSymbolToInterfaceOrClassDeclaration(exportedSymbol); + } + } + } + } + return undefined; } diff --git a/test/incremental-update/associations/inheritance.test.ts b/test/incremental-update/associations/inheritance.test.ts index b4e5ac34..0a25b200 100644 --- a/test/incremental-update/associations/inheritance.test.ts +++ b/test/incremental-update/associations/inheritance.test.ts @@ -126,4 +126,69 @@ describe('Change the inheritance in a single file', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); + + it('should add new inheritance association between class and interface', () => { + // arrange + // const sourceCodeWithInterfaceWithoutInheritance = ` + // interface ${superClassName} { } + + // class ${subClassName} { } + // `; + const sourceCodeWithInterfaceWithoutInheritance = ` + interface ${superClassName} { } + interface A { } + + class ${subClassName} { } + `; + + const sourceCodeWithInterfaceInheritance = ` + interface ${superClassName} { } + interface A { } + + class ${subClassName} implements ${superClassName}, A { } + `; + + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder.addSourceFile(sourceFileName, sourceCodeWithInterfaceWithoutInheritance); + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInterfaceInheritance); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithInterfaceInheritance); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should add new inheritance association between 2 interfaces', () => { + // arrange + const sourceCodeWithInterfaceWithoutInheritance = ` + interface ${superClassName} { } + + interface ${subClassName} { } + `; + + const sourceCodeWithInterfaceInheritance = ` + interface ${superClassName} { } + + interface ${subClassName} extends ${superClassName} { } + `; + + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder.addSourceFile(sourceFileName, sourceCodeWithInterfaceWithoutInheritance); + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, sourceCodeWithInterfaceInheritance); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModel(sourceFileName, sourceCodeWithInterfaceInheritance); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); }); \ No newline at end of file diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 3affd7ad..3447bce5 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -1,4 +1,4 @@ -import { FamixBaseElement } from "../../src"; +import { FamixBaseElement, Inheritance } from "../../src"; import { FamixRepository } from "../../src"; import { Class, PrimitiveType } from "../../src"; @@ -6,7 +6,9 @@ const classCompareFunction = (actual: FamixBaseElement, expected: FamixBaseEleme const actualAsClass = actual as Class; const expectedAsClass = expected as Class; - return actualAsClass.fullyQualifiedName === expectedAsClass.fullyQualifiedName; + return actualAsClass.fullyQualifiedName === expectedAsClass.fullyQualifiedName + && actualAsClass.subInheritances.size === expectedAsClass.subInheritances.size + && actualAsClass.superInheritances.size === expectedAsClass.superInheritances.size; // TODO: add more properties to compare }; @@ -17,6 +19,14 @@ const primitiveTypeCompareFunction = (actual: FamixBaseElement, expected: FamixB return actualAsPrimitiveType.fullyQualifiedName === expectedAsPrimitiveType.fullyQualifiedName; }; +const inheritanceCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { + const actualAsInheritance = actual as Inheritance; + const expectedAsInheritance = expected as Inheritance; + + return actualAsInheritance.superclass.fullyQualifiedName === expectedAsInheritance.superclass.fullyQualifiedName + && actualAsInheritance.subclass.fullyQualifiedName === expectedAsInheritance.subclass.fullyQualifiedName; +}; + export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, expected: FamixRepository) => { // TODO: use the expectElementsToBeSame for more types // TODO: test cyclomatic complexity @@ -38,6 +48,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "ImportClause"); // expectElementsToBeEqualSize(actual, expected, "IndexedFileAnchor"); expectElementsToBeEqualSize(actual, expected, "Inheritance"); + expectElementsToBeSame(actual, expected, "Inheritance", inheritanceCompareFunction); expectElementsToBeEqualSize(actual, expected, "Interface"); expectElementsToBeEqualSize(actual, expected, "Invocation"); expectElementsToBeEqualSize(actual, expected, "Method"); diff --git a/test/testUtils.ts b/test/testUtils.ts index 9e2f8583..c2f5deec 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -7,7 +7,7 @@ export const project = createProject(); export function createProject(): Project { return new Project({ compilerOptions: { - baseUrl: "", + baseUrl: ".", }, useInMemoryFileSystem: true, }); From b7304910ee2ec12077ff769cf5123ac944555d84 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:31:49 -0400 Subject: [PATCH 03/25] Add a FullyQualifiedNameEntity interface --- src/famix_functions/EntityDictionary.ts | 7 +++---- src/lib/famix/model/famix/inheritance.ts | 7 ++++++- src/lib/famix/model/famix/named_entity.ts | 3 ++- .../famix/model/interfaces/fully_qualified_name_entity.ts | 3 +++ src/lib/famix/model/interfaces/index.ts | 1 + 5 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/lib/famix/model/interfaces/fully_qualified_name_entity.ts create mode 100644 src/lib/famix/model/interfaces/index.ts diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index b4e9701d..eb80d841 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -18,6 +18,7 @@ import path from "path"; import { convertToRelativePath } from "./helpers_path"; import { SourceFileDataMap } from "./SourceFileData"; import { getFamixIndexFileAnchorFileName } from "./famixIndexFileAnchorHelper"; +import { FullyQualifiedNameEntity } from "src/lib/famix/model/interfaces"; export type TSMorphObjectType = ImportDeclaration | ImportEqualsDeclaration | SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | TypeParameterDeclaration | Identifier | Decorator | GetAccessorDeclaration | SetAccessorDeclaration | ImportSpecifier | CommentRange | EnumDeclaration | EnumMember | TypeAliasDeclaration | ExpressionWithTypeArguments | TSMorphParametricType; @@ -135,11 +136,9 @@ export class EntityDictionary { */ public makeFamixIndexFileAnchor(sourceElement: TSMorphObjectType, famixElement: Famix.SourcedEntity): void { // Famix.Comment is not a named entity (does not have a fullyQualifiedName) - if (!(famixElement instanceof Famix.Comment) - // TODO: consider better approach for the associations, split the NamedEntity class into 2, extend it from inheritance - && !(famixElement instanceof Famix.Inheritance)) { // must be a named entity + if (!(famixElement instanceof Famix.Comment)) { // must be a named entity // insanity check: named entities should have fullyQualifiedName - const fullyQualifiedName = (famixElement as Famix.NamedEntity).fullyQualifiedName; + const fullyQualifiedName = (famixElement as unknown as FullyQualifiedNameEntity).fullyQualifiedName; if (!fullyQualifiedName || fullyQualifiedName === this.UNKNOWN_VALUE) { throw new Error(`Famix element ${famixElement.constructor.name} has no valid fullyQualifiedName.`); } diff --git a/src/lib/famix/model/famix/inheritance.ts b/src/lib/famix/model/famix/inheritance.ts index aa5bb6e4..3c9138cb 100644 --- a/src/lib/famix/model/famix/inheritance.ts +++ b/src/lib/famix/model/famix/inheritance.ts @@ -1,9 +1,10 @@ import { FamixJSONExporter } from "../../famix_JSON_exporter"; +import { FullyQualifiedNameEntity } from "../interfaces"; import { Class } from "./class"; import { Interface } from "./interface"; import { SourcedEntity } from "./sourced_entity"; -export class Inheritance extends SourcedEntity { +export class Inheritance extends SourcedEntity implements FullyQualifiedNameEntity { private _superclass!: Class | Interface; private _subclass!: Class | Interface; @@ -37,4 +38,8 @@ export class Inheritance extends SourcedEntity { this._subclass = subclass; subclass.addSuperInheritance(this); } + + get fullyQualifiedName(): string { + return `${this.subclass.fullyQualifiedName} extends ${this.superclass.fullyQualifiedName}`; + } } diff --git a/src/lib/famix/model/famix/named_entity.ts b/src/lib/famix/model/famix/named_entity.ts index daf03d8f..13a17bc3 100644 --- a/src/lib/famix/model/famix/named_entity.ts +++ b/src/lib/famix/model/famix/named_entity.ts @@ -4,8 +4,9 @@ import { Invocation } from "./invocation"; import { ImportClause } from "./import_clause"; import { Alias } from "./alias"; import { Decorator } from "./decorator"; +import { FullyQualifiedNameEntity } from "../interfaces"; -export class NamedEntity extends SourcedEntity { +export class NamedEntity extends SourcedEntity implements FullyQualifiedNameEntity { private _fullyQualifiedName!: string; private _receivedInvocations: Set = new Set(); diff --git a/src/lib/famix/model/interfaces/fully_qualified_name_entity.ts b/src/lib/famix/model/interfaces/fully_qualified_name_entity.ts new file mode 100644 index 00000000..ab0a5c04 --- /dev/null +++ b/src/lib/famix/model/interfaces/fully_qualified_name_entity.ts @@ -0,0 +1,3 @@ +export interface FullyQualifiedNameEntity { + get fullyQualifiedName(): string; +} \ No newline at end of file diff --git a/src/lib/famix/model/interfaces/index.ts b/src/lib/famix/model/interfaces/index.ts new file mode 100644 index 00000000..6a192742 --- /dev/null +++ b/src/lib/famix/model/interfaces/index.ts @@ -0,0 +1 @@ +export * from './fully_qualified_name_entity'; \ No newline at end of file From 71c4735803a7dcaadd6d9126873db0b037e073c0 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:48:36 -0400 Subject: [PATCH 04/25] Add catching the error for the extension incremental update --- .../onDidChangeWatchedFilesHandler.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts b/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts index df4f0296..f6c93329 100644 --- a/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts +++ b/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts @@ -14,10 +14,16 @@ export const onDidChangeWatchedFiles = async ( const mapSlice = fileChangesMap.getAndClearFileChangesMap(); // TODO: ensure that there is no race condition (when new changes are added while we are processing the previous ones) - await famixProjectManager.updateFamixModelIncrementally(mapSlice); - const exportResult = await famixProjectManager.generateNewJsonForFamixModel(); - if (exportResult.isErr()) { - connection.window.showErrorMessage(exportResult.error.message); + try { + await famixProjectManager.updateFamixModelIncrementally(mapSlice); + + const exportResult = await famixProjectManager.generateNewJsonForFamixModel(); + if (exportResult.isErr()) { + connection.window.showErrorMessage(exportResult.error.message); + return; + } + } catch { + connection.window.showErrorMessage(`Error processing file changes.`); return; } }; From c863702242c32d7f2162af43dd4fd1d9a11abbc0 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:13:31 -0400 Subject: [PATCH 05/25] Update test/incremental-update/incrementalUpdateExpect.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/incremental-update/incrementalUpdateExpect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 3447bce5..36d7b995 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -6,9 +6,9 @@ const classCompareFunction = (actual: FamixBaseElement, expected: FamixBaseEleme const actualAsClass = actual as Class; const expectedAsClass = expected as Class; - return actualAsClass.fullyQualifiedName === expectedAsClass.fullyQualifiedName - && actualAsClass.subInheritances.size === expectedAsClass.subInheritances.size - && actualAsClass.superInheritances.size === expectedAsClass.superInheritances.size; + return actualAsClass.fullyQualifiedName === expectedAsClass.fullyQualifiedName && + actualAsClass.subInheritances.size === expectedAsClass.subInheritances.size && + actualAsClass.superInheritances.size === expectedAsClass.superInheritances.size; // TODO: add more properties to compare }; From 260619b7cb8a77ee09858ca4d95f6e0ea608c83d Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:14:33 -0400 Subject: [PATCH 06/25] Update vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/eventHandlers/onDidChangeWatchedFilesHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts b/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts index f6c93329..d4bdfd4b 100644 --- a/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts +++ b/vscode-extension/server/src/eventHandlers/onDidChangeWatchedFilesHandler.ts @@ -22,8 +22,8 @@ export const onDidChangeWatchedFiles = async ( connection.window.showErrorMessage(exportResult.error.message); return; } - } catch { - connection.window.showErrorMessage(`Error processing file changes.`); + } catch (error) { + connection.window.showErrorMessage(`Error processing file changes: ${error}`); return; } }; From 53015051873ca1003d0d295c353895fdb2ab24c9 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:23:28 -0400 Subject: [PATCH 07/25] Remove the famixInterfaces list from the repository --- src/lib/famix/famix_repository.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index db105908..6670bec2 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -11,7 +11,7 @@ export class FamixRepository { private elements = new Set(); // All Famix elements // DO WE NEED THESE SETS? THEY ARE ONLY USED IN METHODS THAT ARE USED IN TESTS // private famixClasses = new Set(); // All Famix classes - private famixInterfaces = new Set(); // All Famix interfaces + // private famixInterfaces = new Set(); // All Famix interfaces private famixModules = new Set(); // All Famix namespaces private famixMethods = new Set(); // All Famix methods private famixVariables = new Set(); // All Famix variables @@ -96,10 +96,10 @@ export class FamixRepository { this.elements.delete(entity); // if (entity instanceof Class) { // this.famixClasses.delete(entity); + // } else if (entity instanceof Interface) { + // this.famixInterfaces.delete(entity); // } else - if (entity instanceof Interface) { - this.famixInterfaces.delete(entity); - } else if (entity instanceof Module) { + if (entity instanceof Module) { this.famixModules.delete(entity); } else if (entity instanceof Variable) { this.famixVariables.delete(entity); @@ -183,7 +183,9 @@ export class FamixRepository { * @returns The Famix interface corresponding to the name or undefined if it doesn't exist */ public _getFamixInterface(fullyQualifiedName: string): Interface | undefined { - return Array.from(this.famixInterfaces.values()).find(ns => ns.fullyQualifiedName === fullyQualifiedName); + return Array.from(this.elements.values()) + .filter(e => e instanceof Interface) + .find(ns => ns.fullyQualifiedName === fullyQualifiedName); } /** @@ -291,10 +293,10 @@ export class FamixRepository { logger.debug(`Adding Famix element ${element.constructor.name} with id ${element.id}`); // if (element instanceof Class) { // this.famixClasses.add(element); + // } else if (element instanceof Interface) { + // this.famixInterfaces.add(element); // } else - if (element instanceof Interface) { - this.famixInterfaces.add(element); - } else if (element instanceof Module) { + if (element instanceof Module) { this.famixModules.add(element); } else if (element instanceof Variable) { this.famixVariables.add(element); From 8c9d2828c12ed9e82a952207eb464cb55172c338 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:02:48 -0400 Subject: [PATCH 08/25] Change ensure to create for inheritance famix element --- src/analyze_functions/process_functions.ts | 8 ++++---- src/famix_functions/EntityDictionary.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 90badd5e..b865d356 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -928,14 +928,14 @@ export class TypeScriptToFamixProcessor { logger.debug(`Checking class inheritance for ${cls.getName()}`); const baseClass = cls.getBaseClass(); if (baseClass !== undefined) { - this.entityDictionary.ensureFamixClassToClassInheritance(cls, baseClass); + this.entityDictionary.createFamixClassToClassInheritance(cls, baseClass); logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), extClass: ${baseClass.getName()}, (${baseClass.getType().getText()})`); } // this is false when the class extends an undefined class else { // check for "extends" of unresolved class const undefinedExtendedClass = cls.getExtends(); if (undefinedExtendedClass) { - this.entityDictionary.ensureFamixClassToClassInheritance(cls, undefinedExtendedClass); + this.entityDictionary.createFamixClassToClassInheritance(cls, undefinedExtendedClass); logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), undefinedExtendedClass: ${undefinedExtendedClass.getText()}`); } } @@ -943,7 +943,7 @@ export class TypeScriptToFamixProcessor { logger.debug(`Checking interface inheritance for ${cls.getName()}`); cls.getImplements().forEach(implementedIF => { - this.entityDictionary.ensureFamixInterfaceInheritance(cls, implementedIF); + this.entityDictionary.createFamixInterfaceInheritance(cls, implementedIF); logger.debug(`class: ${cls.getName()}, (${cls.getType().getText()}), impInter: ${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getName() : implementedIF.getExpression().getText()}, (${(implementedIF instanceof InterfaceDeclaration) ? implementedIF.getType().getText() : implementedIF.getExpression().getText()})`); }); } @@ -953,7 +953,7 @@ export class TypeScriptToFamixProcessor { logger.debug(`Checking interface inheritance for ${interFace.getName()}`); interFace.getExtends().forEach(extendedInterface => { - this.entityDictionary.ensureFamixInterfaceInheritance(interFace, extendedInterface); + this.entityDictionary.createFamixInterfaceInheritance(interFace, extendedInterface); logger.debug(`interFace: ${interFace.getName()}, (${interFace.getType().getText()}), extendedInterface: ${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getName() : extendedInterface.getExpression().getText()}, (${(extendedInterface instanceof InterfaceDeclaration) ? extendedInterface.getType().getText() : extendedInterface.getExpression().getText()})`); }); } diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index eb80d841..8047519c 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -1272,7 +1272,7 @@ export class EntityDictionary { this.fmxElementObjectMap.set(fmxInvocation,nodeReferringToInvocable); } - public ensureFamixClassToClassInheritance( + public createFamixClassToClassInheritance( subClass: ClassDeclaration, superClass: ClassDeclaration | ExpressionWithTypeArguments ) { const subClassFamix = this.ensureFamixClass(subClass); @@ -1297,7 +1297,7 @@ export class EntityDictionary { this.createFamixInheritance(subClassFamix, superClassFamix, subClass); } - public ensureFamixInterfaceInheritance( + public createFamixInterfaceInheritance( subClassOrInterface: ClassDeclaration | InterfaceDeclaration, superInterface: InterfaceDeclaration | ExpressionWithTypeArguments ) { const getSubFamixElement = () => { From 9d864239bdf56115e5387931364417fe38c4acd3 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:23:14 -0400 Subject: [PATCH 09/25] Remove the comment --- src/famix_functions/helpersTsMorphElementsProcessing.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/famix_functions/helpersTsMorphElementsProcessing.ts b/src/famix_functions/helpersTsMorphElementsProcessing.ts index 2745ac43..c7a4d8e7 100644 --- a/src/famix_functions/helpersTsMorphElementsProcessing.ts +++ b/src/famix_functions/helpersTsMorphElementsProcessing.ts @@ -78,7 +78,6 @@ function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): Inte const importSpecifier = declaration as ImportSpecifier; const importDeclaration = importSpecifier.getImportDeclaration(); const moduleSpecifier = importDeclaration.getModuleSpecifierSourceFile(); - // importDeclaration. if (moduleSpecifier) { const exportedSymbols = moduleSpecifier.getExportSymbols(); From bedc15690ef453147938b77b073d25a903863cd3 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:55:44 -0400 Subject: [PATCH 10/25] Update README.md --- vscode-extension/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vscode-extension/README.md b/vscode-extension/README.md index ec0b102d..de9e5f6c 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -20,8 +20,12 @@ ts2famix - Run `npm install` in the `ts2famix` folder - Run `npm run build` in the `ts2famix` folder to build the project ### Building the vscode-extension -- Run `npm install` in the `vscode-extension` folder. This installs all necessary npm modules in both the client and server folder -- Open VS Code on the `vscode-extension` folder. It should be open as a workspace (root directory). +- Run `npm install` in the `vscode-extension` folder. This installs all necessary npm modules in both the client and server folder, then open VS Code on the `vscode-extension` folder. It should be open as a workspace (root directory): +``` +cd vscode-extension +npm install +code . +``` - Press Ctrl+Shift+B to start compiling the client and server in [watch mode](https://code.visualstudio.com/docs/editor/tasks#:~:text=The%20first%20entry%20executes,the%20HelloWorld.js%20file.). - Switch to the Run and Debug View in the Sidebar (Ctrl+Shift+D). - Select `Launch Client` from the drop down (if it is not already). From c98fd73872033dc5af829ff576757c2da44a4cc2 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:22:26 -0400 Subject: [PATCH 11/25] Split SourcedEntity class into 2 classes: SourcedEntity and EntityWithSourceAnchor --- src/lib/famix/famix_repository.ts | 3 ++- src/lib/famix/model/famix/inheritance.ts | 4 +-- src/lib/famix/model/famix/source_anchor.ts | 6 ++--- src/lib/famix/model/famix/sourced_entity.ts | 28 +++++++++++---------- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index 6670bec2..1925ed54 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -3,6 +3,7 @@ import { Class, Interface, Variable, Method, ArrowFunction, Function as FamixFun import * as Famix from "./model/famix"; import { TSMorphObjectType } from "../../famix_functions/EntityDictionary"; import { logger } from "../../analyze"; +import { EntityWithSourceAnchor } from "./model/famix/sourced_entity"; /** * This class is used to store all Famix elements @@ -73,7 +74,7 @@ export class FamixRepository { private getElementsBySourceFile(sourceFile: string): FamixBaseElement[] { return Array.from(this.elements.values()).filter(e => { - if (e instanceof Famix.SourcedEntity && e.sourceAnchor && e.sourceAnchor instanceof Famix.IndexedFileAnchor) { + if (e instanceof EntityWithSourceAnchor && e.sourceAnchor && e.sourceAnchor instanceof Famix.IndexedFileAnchor) { return e.sourceAnchor.fileName === sourceFile; } else if (e instanceof Famix.IndexedFileAnchor) { return e.fileName === sourceFile; diff --git a/src/lib/famix/model/famix/inheritance.ts b/src/lib/famix/model/famix/inheritance.ts index 3c9138cb..cc719563 100644 --- a/src/lib/famix/model/famix/inheritance.ts +++ b/src/lib/famix/model/famix/inheritance.ts @@ -2,9 +2,9 @@ import { FamixJSONExporter } from "../../famix_JSON_exporter"; import { FullyQualifiedNameEntity } from "../interfaces"; import { Class } from "./class"; import { Interface } from "./interface"; -import { SourcedEntity } from "./sourced_entity"; +import { EntityWithSourceAnchor } from "./sourced_entity"; -export class Inheritance extends SourcedEntity implements FullyQualifiedNameEntity { +export class Inheritance extends EntityWithSourceAnchor implements FullyQualifiedNameEntity { private _superclass!: Class | Interface; private _subclass!: Class | Interface; diff --git a/src/lib/famix/model/famix/source_anchor.ts b/src/lib/famix/model/famix/source_anchor.ts index 641433dd..4901172a 100644 --- a/src/lib/famix/model/famix/source_anchor.ts +++ b/src/lib/famix/model/famix/source_anchor.ts @@ -1,10 +1,10 @@ import { FamixJSONExporter } from "../../famix_JSON_exporter"; import { Entity } from "./entity"; -import { SourcedEntity } from "./sourced_entity"; +import { EntityWithSourceAnchor } from "./sourced_entity"; export class SourceAnchor extends Entity { - private _element!: SourcedEntity; + private _element!: EntityWithSourceAnchor; public getJSON(): string { const json: FamixJSONExporter = new FamixJSONExporter("SourceAnchor", this); @@ -21,7 +21,7 @@ export class SourceAnchor extends Entity { return this._element; } - set element(element: SourcedEntity) { + set element(element: EntityWithSourceAnchor) { if (this._element === undefined) { this._element = element; element.sourceAnchor = this; diff --git a/src/lib/famix/model/famix/sourced_entity.ts b/src/lib/famix/model/famix/sourced_entity.ts index e4a514e9..51ea1115 100644 --- a/src/lib/famix/model/famix/sourced_entity.ts +++ b/src/lib/famix/model/famix/sourced_entity.ts @@ -5,10 +5,23 @@ import { Comment } from "./comment"; import { SourceAnchor } from "./source_anchor"; import { logger } from "../../../../analyze"; -export class SourcedEntity extends Entity { +export abstract class EntityWithSourceAnchor extends Entity { + protected _sourceAnchor!: SourceAnchor; + get sourceAnchor() { + return this._sourceAnchor; + } + + set sourceAnchor(sourceAnchor: SourceAnchor) { + if (this._sourceAnchor === undefined) { + this._sourceAnchor = sourceAnchor; + sourceAnchor.element = this; + } + } +} + +export class SourcedEntity extends EntityWithSourceAnchor { private _isStub!: boolean; - private _sourceAnchor!: SourceAnchor; private _comments: Set = new Set(); public addComment(comment: Comment): void { @@ -44,17 +57,6 @@ export class SourcedEntity extends Entity { this._isStub = isStub; } - get sourceAnchor() { - return this._sourceAnchor; - } - - set sourceAnchor(sourceAnchor: SourceAnchor) { - if (this._sourceAnchor === undefined) { - this._sourceAnchor = sourceAnchor; - sourceAnchor.element = this; - } - } - get comments() { return this._comments; } From d73749093a20218b23ce6f4e0da4f891888b456f Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:10:04 -0400 Subject: [PATCH 12/25] Add tests for import clause --- .../helpersTsMorphElementsProcessing.ts | 12 + test/helpersTests/isSourceFileAModule.test.ts | 149 +++++++++ test/importClauseDefaultExports.test.ts | 302 ++++++++++++++++++ test/importClauseEqualsDeclaration.test.ts | 299 +++++++++++++++++ test/importClauseNamedImport.test.ts | 164 ++++++++++ test/importClauseNamespaceImport.test.ts | 135 ++++++++ .../importClauseEqualsDeclaraton.test.ts | 127 ++++++++ .../importClauseNamedImport.test.ts | 125 ++++++++ .../associations/importClauseReExport.test.ts | 147 +++++++++ .../associations/modulesInheritance.test.ts | 86 +++-- 10 files changed, 1497 insertions(+), 49 deletions(-) create mode 100644 test/helpersTests/isSourceFileAModule.test.ts create mode 100644 test/importClauseDefaultExports.test.ts create mode 100644 test/importClauseEqualsDeclaration.test.ts create mode 100644 test/importClauseNamedImport.test.ts create mode 100644 test/importClauseNamespaceImport.test.ts create mode 100644 test/incremental-update/associations/importClauseEqualsDeclaraton.test.ts create mode 100644 test/incremental-update/associations/importClauseNamedImport.test.ts create mode 100644 test/incremental-update/associations/importClauseReExport.test.ts diff --git a/src/famix_functions/helpersTsMorphElementsProcessing.ts b/src/famix_functions/helpersTsMorphElementsProcessing.ts index c7a4d8e7..7f63a100 100644 --- a/src/famix_functions/helpersTsMorphElementsProcessing.ts +++ b/src/famix_functions/helpersTsMorphElementsProcessing.ts @@ -27,6 +27,18 @@ export function getArrowFunctionClasses(f: ArrowFunction): ClassDeclaration[] { return classes; } +/** + * Checks if the file has any imports or exports to be considered a module + * @param sourceFile A source file + * @returns A boolean indicating if the file is a module + */ +export function isSourceFileAModule(sourceFile: SourceFile): boolean { + return sourceFile.getImportDeclarations().length > 0 || + sourceFile.getExportedDeclarations().size > 0 || + sourceFile.getExportDeclarations().length > 0 || + sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration).length > 0; +} + // NOTE: Finding the symbol may not work when used bare import without baseUrl // e.g. import { MyInterface } from "outsideInterface"; will not work if baseUrl is not set export function getInterfaceOrClassDeclarationFromExpression(expression: ExpressionWithTypeArguments): InterfaceDeclaration | ClassDeclaration | undefined { diff --git a/test/helpersTests/isSourceFileAModule.test.ts b/test/helpersTests/isSourceFileAModule.test.ts new file mode 100644 index 00000000..33f4457b --- /dev/null +++ b/test/helpersTests/isSourceFileAModule.test.ts @@ -0,0 +1,149 @@ +import { Project } from 'ts-morph'; +import { isSourceFileAModule } from '../../src/famix_functions/helpersTsMorphElementsProcessing'; + +describe('isSourceFileAModule', () => { + let project: Project; + + beforeEach(() => { + project = new Project({ + useInMemoryFileSystem: true, + }); + }); + + afterEach(() => { + project.getSourceFiles().forEach(sourceFile => sourceFile.delete()); + }); + + it('should return true for file with import declarations', () => { + const sourceFile = project.createSourceFile('test.ts', ` + import { Component } from 'react'; + + class MyClass { + // some code + } + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with import equals declaration', () => { + const sourceFile = project.createSourceFile('test.ts', ` + import fs = require('fs'); + + class MyClass { + // some code + } + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with namespace import', () => { + const sourceFile = project.createSourceFile('test.ts', ` + import * as React from 'react'; + + class MyComponent { + // some code + } + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with export declarations', () => { + const sourceFile = project.createSourceFile('test.ts', ` + class MyClass { + // some code + } + + export { MyClass }; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with default export', () => { + const sourceFile = project.createSourceFile('test.ts', ` + class MyClass { + // some code + } + + export default MyClass; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with multiple module indicators', () => { + const sourceFile = project.createSourceFile('test.ts', ` + import { Component } from 'react'; + import fs = require('fs'); + + class MyClass { + // some code + } + + export { MyClass }; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return false for file without any module indicators', () => { + const sourceFile = project.createSourceFile('test.ts', ` + class MyClass { + // some code + } + `); + + expect(isSourceFileAModule(sourceFile)).toBe(false); + }); + + it('should return true for file with re-export', () => { + const sourceFile = project.createSourceFile('test.ts', ` + export { MyClass } from './otherModule'; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with re-export default', () => { + const sourceFile = project.createSourceFile('test.ts', ` + export { default as MyClass } from './otherModule'; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with export all', () => { + const sourceFile = project.createSourceFile('test.ts', ` + export * from './otherModule'; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with type-only imports', () => { + const sourceFile = project.createSourceFile('test.ts', ` + import type { MyType } from './types'; + + class MyClass implements MyType { + // some code + } + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); + + it('should return true for file with type-only exports', () => { + const sourceFile = project.createSourceFile('test.ts', ` + type MyType = { + name: string; + }; + + export type { MyType }; + `); + + expect(isSourceFileAModule(sourceFile)).toBe(true); + }); +}); diff --git a/test/importClauseDefaultExports.test.ts b/test/importClauseDefaultExports.test.ts new file mode 100644 index 00000000..1ae15d79 --- /dev/null +++ b/test/importClauseDefaultExports.test.ts @@ -0,0 +1,302 @@ +import { Class, ImportClause, Module, StructuralEntity } from '../src'; +import { Importer } from '../src/analyze'; +import { createProject } from './testUtils'; + +describe('Import Clause Default Exports', () => { + it("should work with default exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import Animal from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with default exports with custom names", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import Pet from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with default exports when the entity is not exported as default", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import Animal from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with multiple default exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/moduleA.ts", + `export default class ClassA {} + `); + + project.createSourceFile("/moduleB.ts", + `export default function helperB() {} + `); + + project.createSourceFile("/importingFile.ts", + `import ClassA from './moduleA'; + import helperB from './moduleB'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 2; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + + const classAImport = importClauses.find(ic => ic.importedEntity.name === 'ClassA'); + const helperBImport = importClauses.find(ic => ic.importedEntity.name === 'helperB'); + + expect(classAImport).toBeTruthy(); + expect(helperBImport).toBeTruthy(); + expect(classAImport?.importingEntity?.name).toBe('importingFile.ts'); + expect(helperBImport?.importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with default exports for constants", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `const config = { apiUrl: 'https://api.example.com' }; + export default config; + `); + + project.createSourceFile("/importingFile.ts", + `import appConfig from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with named default exports (import { default as SomeValue })", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { default as Pet } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with named default exports when entity is not exported as default", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { default as Pet } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with default exports and re-export", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/originalFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/reExportingFile.ts", + `export { default } from './originalFile'; + `); + + project.createSourceFile("/importingFile.ts", + `import Animal from './reExportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with default exports re-exported with alias", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/originalFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/reExportingFile.ts", + `export { default as Pet } from './originalFile'; + `); + + project.createSourceFile("/importingFile.ts", + `import { Pet } from './reExportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); +}); \ No newline at end of file diff --git a/test/importClauseEqualsDeclaration.test.ts b/test/importClauseEqualsDeclaration.test.ts new file mode 100644 index 00000000..7df0fb1a --- /dev/null +++ b/test/importClauseEqualsDeclaration.test.ts @@ -0,0 +1,299 @@ +import { Class, ImportClause, Module, StructuralEntity } from '../src'; +import { Function as FamixFunction } from '../src/lib/famix/model/famix/function'; +import { Importer } from '../src/analyze'; +import { createProject } from './testUtils'; + +describe('Import Clause Equals Declarations', () => { + it("should work with import equals declaration for exported class", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import Animal = require('./exportingFile'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with import equals declaration for exported namespace", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export namespace Utils { + export function helper() {} + } + `); + + project.createSourceFile("/importingFile.ts", + `import Utils = require('./exportingFile'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(importClauses[0].importedEntity.name).toBe('Utils'); + }); + + it("should work with import equals declaration when the entity is not exported", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import Animal = require('./exportingFile'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with multiple import equals declarations", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/moduleA.ts", + `export default class ClassA {} + `); + + project.createSourceFile("/moduleB.ts", + `export namespace UtilsB { + export function helper() {} + } + `); + + project.createSourceFile("/importingFile.ts", + `import ClassA = require('./moduleA'); + import UtilsB = require('./moduleB'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 2; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + + const classAImport = importClauses.find(ic => ic.importedEntity.name === 'ClassA'); + const utilsBImport = importClauses.find(ic => ic.importedEntity.name === 'UtilsB'); + + expect(classAImport).toBeTruthy(); + expect(utilsBImport).toBeTruthy(); + expect(classAImport?.importingEntity?.name).toBe('importingFile.ts'); + expect(utilsBImport?.importingEntity?.name).toBe('importingFile.ts'); + }); + + // TODO: how should we handle this case? + it("should work with import equals declaration for mixed export types", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/mixedExports.ts", + `export class ExportedClass {} + export const exportedVariable = 42; + export function exportedFunction() {} + `); + + project.createSourceFile("/importingFile.ts", + `import MixedModule = require('./mixedExports'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(importClauses[0].importedEntity.name).toBe('MixedModule'); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + // NOTE: So when you type const x = require('x'), TypeScript is going to complain that it doesn't know what require is. + // You need to install @types/node package to install type definitions for the CommonJS module system + // in order to work with it from the TypeScript. + + // TODO: the files with only require statement should be a module??? (it does not have import/export but it is still a module?) + // There should be import clauses for it + it("should work with const require imports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/maths.ts", + `export function squareTwo() { return 4; } + export function cubeThree() { return 27; } + export const PI = 3.14; + `); + + project.createSourceFile("/importingFile.ts", + `const mathModule = require("./maths"); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 2; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_FUNCTIONS = 2; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const functionsList = Array.from(fmxRep._getAllEntitiesWithType('Function')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(functionsList.length).toBe(NUMBER_OF_FUNCTIONS); + + // Check that the destructured imports reference the correct entities + const squareTwoImport = importClauses.find(ic => ic.importedEntity.name === 'squareTwo'); + const cubeThreeImport = importClauses.find(ic => ic.importedEntity.name === 'cubeThree'); + + expect(squareTwoImport).toBeTruthy(); + expect(cubeThreeImport).toBeTruthy(); + expect(squareTwoImport?.importingEntity?.name).toBe('importingFile.ts'); + expect(cubeThreeImport?.importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with destructuring require imports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/maths.ts", + `export function squareTwo() { return 4; } + export function cubeThree() { return 27; } + export const PI = 3.14; + `); + + project.createSourceFile("/importingFile.ts", + `const { squareTwo } = require("./maths"); + const { cubeThree, PI } = require("./maths"); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 2; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_FUNCTIONS = 2; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const functionsList = Array.from(fmxRep._getAllEntitiesWithType('Function')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(functionsList.length).toBe(NUMBER_OF_FUNCTIONS); + + // Check that the destructured imports reference the correct entities + const squareTwoImport = importClauses.find(ic => ic.importedEntity.name === 'squareTwo'); + const cubeThreeImport = importClauses.find(ic => ic.importedEntity.name === 'cubeThree'); + + expect(squareTwoImport).toBeTruthy(); + expect(cubeThreeImport).toBeTruthy(); + expect(squareTwoImport?.importingEntity?.name).toBe('importingFile.ts'); + expect(cubeThreeImport?.importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with import equals declaration and re-export", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/originalFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/reExportingFile.ts", + `export { default } from './originalFile'; + `); + + project.createSourceFile("/importingFile.ts", + `import Animal = require('./reExportingFile'); + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); +}); \ No newline at end of file diff --git a/test/importClauseNamedImport.test.ts b/test/importClauseNamedImport.test.ts new file mode 100644 index 00000000..74372b3a --- /dev/null +++ b/test/importClauseNamedImport.test.ts @@ -0,0 +1,164 @@ +import { Class, ImportClause, Module, NamedEntity } from '../src'; +import { Importer } from '../src/analyze'; +import { createProject } from './testUtils'; + +describe('Import Clause Named Imports', () => { + it("should work with named imports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { Animal } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_NAMED_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with named imports with aliases", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { Animal as Pet } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_NAMED_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with named imports when the entity is not exported", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { Animal } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_NAMED_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with named imports with aliases when the entity is not exported", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import { Animal as Pet } from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_NAMED_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with named imports and re-export", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export class Animal {}` + ); + + project.createSourceFile("/reExportingFile.ts", + `export { Animal } from './exportingFile';` + ); + + project.createSourceFile("/importingFile.ts", + `import { Animal } from './reExportingFile';` + ); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_NAMED_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); +}); diff --git a/test/importClauseNamespaceImport.test.ts b/test/importClauseNamespaceImport.test.ts new file mode 100644 index 00000000..549a6c05 --- /dev/null +++ b/test/importClauseNamespaceImport.test.ts @@ -0,0 +1,135 @@ +import { Class, ImportClause, Module, StructuralEntity } from '../src'; +import { Importer } from '../src/analyze'; +import { createProject } from './testUtils'; + +describe('Import Clause Namespace Imports', () => { + it("should work with namespace imports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import * as Utils from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with namespace imports from exported namespace", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export namespace Utils { + export class Helper {} + } + `); + + project.createSourceFile("/importingFile.ts", + `import * as MyUtils from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with namespace imports when module has no exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import * as Utils from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with namespace imports for re-exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/baseModule.ts", + `export class BaseClass {} + export function baseFunction() {} + `); + + project.createSourceFile("/exportingFile.ts", + `export * from './baseModule'; + export class AdditionalClass {} + `); + + project.createSourceFile("/importingFile.ts", + `import * as AllUtils from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 2; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); +}); \ No newline at end of file diff --git a/test/incremental-update/associations/importClauseEqualsDeclaraton.test.ts b/test/incremental-update/associations/importClauseEqualsDeclaraton.test.ts new file mode 100644 index 00000000..e4327b59 --- /dev/null +++ b/test/incremental-update/associations/importClauseEqualsDeclaraton.test.ts @@ -0,0 +1,127 @@ +import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; +import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; + +const exportSourceFileName = 'exportSourceCode.ts'; +const importSourceFileName = 'importSourceCode.ts'; +const existingClassName = 'ExistingClass'; + +describe('Import clause equals declaration tests', () => { + const sourceCodeWithExport = ` + export default class ${existingClassName} { } + `; + + const sourceCodeWithExportChanged = ` + export default class ${existingClassName} { + newMethod() { } + } + `; + + const sourceCodeWithoutImport = ` + class NewClass { } + `; + + const sourceCodeWithImportEquals = ` + import ${existingClassName} = require('./${exportSourceFileName}'); + + class NewClass { } + `; + + const sourceCodeWithImportEqualsChanged = ` + import ${existingClassName} = require('./${exportSourceFileName}'); + + class NewClassChanged { } + `; + + it('should add new import equals declaration association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithoutImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImportEquals); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImportEquals] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should remove an import equals declaration association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImportEquals); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithoutImport); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithoutImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import equals declaration association when export file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImportEquals); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, sourceCodeWithExportChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExportChanged], + [importSourceFileName, sourceCodeWithImportEquals] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import equals declaration association when importing file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImportEquals); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImportEqualsChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImportEqualsChanged] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); \ No newline at end of file diff --git a/test/incremental-update/associations/importClauseNamedImport.test.ts b/test/incremental-update/associations/importClauseNamedImport.test.ts new file mode 100644 index 00000000..9f75208c --- /dev/null +++ b/test/incremental-update/associations/importClauseNamedImport.test.ts @@ -0,0 +1,125 @@ +import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; +import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; + +const exportSourceFileName = 'exportSourceCode.ts'; +const importSourceFileName = 'importSourceCode.ts'; +const existingClassName = 'ExistingClass'; + +describe('Add new classes to a single file', () => { + const sourceCodeWithExport = ` + export class ${existingClassName} { } + `; + + const sourceCodeWithExportChanged = ` + class NewBaseClass { } + export class ${existingClassName} extends NewBaseClass { } + `; + + const sourceCodeWithoutImport = ` + class NewClass { } + `; + + const sourceCodeWithImport = ` + import { ${existingClassName} } from './${exportSourceFileName}'; + + class NewClass { } + `; + + const sourceCodeWithImportChanged = ` + import { ${existingClassName} } from './${exportSourceFileName}'; + + class NewClassChanged { } + `; + + it('should add new import clause association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithoutImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImport); + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should remove an import clause association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithoutImport); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithoutImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import clause association when export file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, sourceCodeWithExportChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExportChanged], + [importSourceFileName, sourceCodeWithImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import clause association when importing file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImportChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImportChanged] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); \ No newline at end of file diff --git a/test/incremental-update/associations/importClauseReExport.test.ts b/test/incremental-update/associations/importClauseReExport.test.ts new file mode 100644 index 00000000..584983d8 --- /dev/null +++ b/test/incremental-update/associations/importClauseReExport.test.ts @@ -0,0 +1,147 @@ +import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; +import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; + +const exportSourceFileName = 'exportSource.ts'; +const reexportSourceFileName = 'reexportSource.ts'; +const importSourceFileName = 'importSource.ts'; +const existingClassName = 'ExistingClass'; + +describe('Re-export functionality with inheritance changes', () => { + const initialExportCode = ` + export class ${existingClassName} { } + `; + + const exportCodeWithInheritance = ` + class BaseClass { } + + export class ${existingClassName} extends BaseClass { } + `; + + const reexportCode = ` + export { ${existingClassName} } from './${exportSourceFileName}'; + `; + + const importCode = ` + import { ${existingClassName} } from './${reexportSourceFileName}'; + + class ConsumerClass extends ${existingClassName} { } + `; + + it('should maintain re-export associations when original export adds inheritance', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - change the original export file to add inheritance + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, exportCodeWithInheritance); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, exportCodeWithInheritance], + [reexportSourceFileName, reexportCode], + [importSourceFileName, importCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should establish correct re-export chain from scratch', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, '') + .addSourceFile(importSourceFileName, ''); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - add re-export + let sourceFile = testProjectBuilder.changeSourceFile(reexportSourceFileName, reexportCode); + let fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // act - add import from re-export + sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, importCode); + fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, reexportCode], + [importSourceFileName, importCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should handle removing re-export while maintaining original export', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - remove re-export + const sourceFile = testProjectBuilder.changeSourceFile(reexportSourceFileName, ''); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, ''], + [importSourceFileName, importCode] + ]); + + // assert + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should update re-export associations when import file changes to use original export', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - change import to use original export instead of re-export + const directImportCode = ` + import { ${existingClassName} } from './${exportSourceFileName}'; + + class ConsumerClass { + private instance: ${existingClassName}; + + constructor() { + this.instance = new ${existingClassName}(); + } + } + `; + + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, directImportCode); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, reexportCode], + [importSourceFileName, directImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); diff --git a/test/incremental-update/associations/modulesInheritance.test.ts b/test/incremental-update/associations/modulesInheritance.test.ts index 3a3f61b3..b0ffedd2 100644 --- a/test/incremental-update/associations/modulesInheritance.test.ts +++ b/test/incremental-update/associations/modulesInheritance.test.ts @@ -1,6 +1,6 @@ import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; -import { createExpectedFamixModelForSeveralFiles } from "../incrementalUpdateTestHelper"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; const sourceFileNameUsesSuper = 'sourceCodeUsesSuper.ts'; const superClassName = 'SuperClass'; @@ -9,34 +9,21 @@ const sourceFileNameSuperClass = `${superClassName}.ts`; const sourceFileNameSubClass = `${subClassName}.ts`; const exportSuperClassCode = ` - export class ${superClassName} { - protected property1: string; - protected method1() {} - } + export class ${superClassName} { } `; const subClassWithoutInheritanceCode = ` - class ${subClassName} { - method2(): number { - return 42; - } - } + class ${subClassName} { } `; const importSubClassWithInheritanceCode = ` import { ${superClassName} } from './${superClassName}'; - class ${subClassName} extends ${superClassName} { - method2(): number { - return 42; - } - } + class ${subClassName} extends ${superClassName} { } `; const exportSuperClassChangedCode = ` - export class ${superClassName} { - protected property1: number; - protected method1Changed() {} - } + class BaseSuperClass { } + export class ${superClassName} extends BaseSuperClass { } `; const fileCodeThatUsesSuperClass = ` @@ -57,7 +44,8 @@ describe('Change the inheritance between several files', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileNameSubClass, importSubClassWithInheritanceCode); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ @@ -79,7 +67,8 @@ describe('Change the inheritance between several files', () => { const sourceFile = testProjectBuilder.changeSourceFile(sourceFileNameSubClass, subClassWithoutInheritanceCode); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ @@ -102,7 +91,8 @@ describe('Change the inheritance between several files', () => { .changeSourceFile(sourceFileNameSuperClass, exportSuperClassChangedCode); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ @@ -127,7 +117,8 @@ describe('Change the inheritance between several files', () => { .changeSourceFile(sourceFileNameSuperClass, exportSuperClassChangedCode); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ @@ -141,20 +132,18 @@ describe('Change the inheritance between several files', () => { it('should handle a chain of the classes with inheritance when super class changed', () => { // arrange - const classACode = `export class classA { - private a: boolean; - }`; - const classACodeChanged = `export class classA { - private a: number; - }`; + const classACode = `export class classA { }`; + const classACodeChanged = ` + import { SomeUndefinedClass } from './unexistingModule.ts'; + export class classA extends SomeUndefinedClass { }`; const classAFileName = 'classA.ts'; const classBCode = `import { classA } from './classA'; - export class classB extends classA {}`; + export class classB extends classA { }`; const classBFileName = 'classB.ts'; const classCCode = `import { classB } from './classB'; - export class classC extends classB {}`; + export class classC extends classB { }`; const classCFileName = 'classC.ts'; @@ -169,7 +158,8 @@ describe('Change the inheritance between several files', () => { .changeSourceFile(classAFileName, classACodeChanged); // act - importer.updateFamixModelIncrementally([sourceFile]); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ @@ -183,27 +173,21 @@ describe('Change the inheritance between several files', () => { it('should handle a chain of the classes with inheritance when super class changed 2 times', () => { // arrange - const classACode = `export class classA { - private a: boolean = true; - }`; - const classACodeChanged = `export class classA { - private a: number = 42; - }`; - const classACodeChangedTwice = `export class classA { - private a: string = 'hello'; - }`; + const classACode = `export class classA { }`; + const classACodeChanged = ` + import { SomeUndefinedClass } from './unexistingModule.ts'; + export class classA extends SomeUndefinedClass { }`; + const classACodeChangedTwice = ` + import { OtherUndefinedClass } from './unexistingModule.ts'; + export class classA extends OtherUndefinedClass { }`; const classAFileName = 'classA.ts'; const classBCode = `import { classA } from './classA'; - export class classB extends classA { - private assignVariable(): void { - const assignedVariable = this.a; - } - }`; + export class classB extends classA { }`; const classBFileName = 'classB.ts'; const classCCode = `import { classB } from './classB'; - export class classC extends classB {}`; + export class classC extends classB { }`; const classCFileName = 'classC.ts'; @@ -218,9 +202,13 @@ describe('Change the inheritance between several files', () => { .changeSourceFile(classAFileName, classACodeChanged); // act - importer.updateFamixModelIncrementally([sourceFile]); + let fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + testProjectBuilder.changeSourceFile(classAFileName, classACodeChangedTwice); - importer.updateFamixModelIncrementally([sourceFile]); + + fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); // assert const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ From 308de346c2723dc1d1ab02ad9b663a77e3b712cf Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:16:02 -0400 Subject: [PATCH 13/25] Implement ImportClause for named imports. Add incremental update for the imported entities (Inheritance and ImportClause). ---------------------------- Still need to resolve the issue with: - re-export - the cases when we choose to create the module over file - finish file and module Famix elements creation, add tests - implement ImportClause for other types of imports --- src/analyze.ts | 48 ++-- src/analyze_functions/process_functions.ts | 145 ++-------- src/famix_functions/EntityDictionary.ts | 251 ++++++------------ .../famixIndexFileAnchorHelper.ts | 6 +- src/helpers/incrementalUpdateHelper.ts | 96 +++++++ src/helpers/index.ts | 2 + src/lib/famix/famix_repository.ts | 42 +-- src/lib/famix/model/famix/import_clause.ts | 9 +- .../incrementalUpdateExpect.ts | 45 +++- 9 files changed, 291 insertions(+), 353 deletions(-) rename src/{famix_functions => helpers}/famixIndexFileAnchorHelper.ts (79%) create mode 100644 src/helpers/incrementalUpdateHelper.ts create mode 100644 src/helpers/index.ts diff --git a/src/analyze.ts b/src/analyze.ts index c38855ce..8c8d33ce 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -5,7 +5,10 @@ import { Logger } from "tslog"; import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/EntityDictionary"; import path from "path"; import { TypeScriptToFamixProcessor } from "./analyze_functions/process_functions"; -import { getFamixIndexFileAnchorFileName } from "./famix_functions/famixIndexFileAnchorHelper"; +import { getFamixIndexFileAnchorFileName } from "./helpers"; +import { isSourceFileAModule } from "./famix_functions/helpersTsMorphElementsProcessing"; +import { FamixBaseElement } from "./lib/famix/famix_base_element"; +import { getDependentAssociations, getSourceFilesToUpdate, removeDependentAssociations } from "./helpers"; export const logger = new Logger({ name: "ts2famix", minLevel: 2 }); @@ -70,29 +73,23 @@ export class Importer { this.processReferences(onlyTypeScriptFiles, onlyTypeScriptFiles); } - private processReferences(sourceFiles: SourceFile[], allExistingSourceFiles: SourceFile[]): void { + private processReferences(sourceFiles: SourceFile[], allExistingSourceFiles: SourceFile[]): void { sourceFiles.forEach(sourceFile => { const fileName = sourceFile.getFilePath(); const accesses = this.processFunctions.accessMap.getBySourceFileName(fileName); const methodsAndFunctionsWithId = this.processFunctions.methodsAndFunctionsWithId.getBySourceFileName(fileName); - // const classes = this.processFunctions.classes.getBySourceFileName(fileName); - // const interfaces = this.processFunctions.interfaces.getBySourceFileName(fileName); - const modules = this.processFunctions.modules.getBySourceFileName(fileName); - const exports = this.processFunctions.listOfExportMaps.getBySourceFileName(fileName); - - // this.entityDictionary.setCurrentSourceFileName(fileName); // TODO: check if it is working correctly - this.processFunctions.processImportClausesForImportEqualsDeclarations(allExistingSourceFiles, exports); - this.processFunctions.processImportClausesForModules(modules, exports); this.processFunctions.processAccesses(accesses); this.processFunctions.processInvocations(methodsAndFunctionsWithId); - // this.processFunctions.processInheritances(classes, interfaces, this.processFunctions.interfaces.getAll()); - // this.processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId); //TODO: fix concretisatoion this.processFunctions.processConcretisations([], [], methodsAndFunctionsWithId); }); + this.processFunctions.processImportClausesForImportEqualsDeclarations(allExistingSourceFiles); + + const modules = sourceFiles.filter(f => isSourceFileAModule(f)); + this.processFunctions.processImportClausesForModules(modules); } /** @@ -132,27 +129,30 @@ export class Importer { } public updateFamixModelIncrementally(sourceFileChangeMap: Map): void { - const allSourceFiles = Array.from(sourceFileChangeMap.values()).flat(); - const sourceFilesToCreateEntities = [ - ...(sourceFileChangeMap.get(SourceFileChangeType.Create) || []), - ...(sourceFileChangeMap.get(SourceFileChangeType.Update) || []), - ]; + const allChangedSourceFiles = Array.from(sourceFileChangeMap.values()).flat(); - allSourceFiles.forEach( + const removedEntities: FamixBaseElement[] = []; + allChangedSourceFiles.forEach( file => { const filePath = getFamixIndexFileAnchorFileName(file.getFilePath(), this.entityDictionary.getAbsolutePath()); - this.entityDictionary.famixRep.removeEntitiesBySourceFile(filePath); - // this.entityDictionary.removeEntitiesBySourceFilePath(filePath); - // this.processFunctions.removeNodesBySourceFile(filePath); + const removed = this.entityDictionary.famixRep.removeEntitiesBySourceFile(filePath); + removedEntities.push(...removed); } ); - this.processFunctions.processFiles(sourceFilesToCreateEntities); + const allSourceFiles = this.project.getSourceFiles(); + const dependentAssociations = getDependentAssociations(removedEntities); + + removeDependentAssociations(this.entityDictionary.famixRep, dependentAssociations); + + const sourceFilesToEnsure = getSourceFilesToUpdate(dependentAssociations, sourceFileChangeMap, allSourceFiles); + + this.processFunctions.processFiles(sourceFilesToEnsure); const sourceFilesToDelete = sourceFileChangeMap.get(SourceFileChangeType.Delete) || []; - const existingSourceFiles = this.project.getSourceFiles().filter( + const existingSourceFiles = allSourceFiles.filter( file => !sourceFilesToDelete.includes(file) ); - this.processReferences(sourceFilesToCreateEntities, existingSourceFiles); + this.processReferences(sourceFilesToEnsure, existingSourceFiles); } diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index b865d356..d9f15470 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -1,11 +1,11 @@ -import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclaration, VariableDeclaration, InterfaceDeclaration, ParameterDeclaration, ConstructorDeclaration, MethodSignature, SourceFile, ModuleDeclaration, PropertyDeclaration, PropertySignature, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ExportedDeclarations, CommentRange, EnumDeclaration, EnumMember, TypeParameterDeclaration, TypeAliasDeclaration, SyntaxKind, FunctionExpression, Block, Identifier, ImportDeclaration, Node, ArrowFunction, Scope, ClassExpression } from "ts-morph"; +import { ClassDeclaration, MethodDeclaration, VariableStatement, FunctionDeclaration, VariableDeclaration, InterfaceDeclaration, ParameterDeclaration, ConstructorDeclaration, MethodSignature, SourceFile, ModuleDeclaration, PropertyDeclaration, PropertySignature, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, CommentRange, EnumDeclaration, EnumMember, TypeParameterDeclaration, TypeAliasDeclaration, SyntaxKind, FunctionExpression, Block, Identifier, ImportDeclaration, Node, ArrowFunction, Scope, ClassExpression } from "ts-morph"; import * as Famix from "../lib/famix/model/famix"; import { calculate } from "../lib/ts-complex/cyclomatic-service"; import * as fs from 'fs'; import { logger } from "../analyze"; import { getFQN } from "../fqn"; import { EntityDictionary, InvocableType } from "../famix_functions/EntityDictionary"; -import { SourceFileDataArray, SourceFileDataMap, SourceFileDataSet } from "../famix_functions/SourceFileData"; +import { SourceFileDataMap, SourceFileDataSet } from "../famix_functions/SourceFileData"; import { getClassesDeclaredInArrowFunctions } from "../famix_functions/helpersTsMorphElementsProcessing"; export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember; @@ -15,25 +15,12 @@ type ContainerTypes = SourceFile | ModuleDeclaration | FunctionDeclaration | Fun type ScopedTypes = Famix.ScriptEntity | Famix.Module | Famix.Function | Famix.Method | Famix.Accessor; -/** - * Checks if the file has any imports or exports to be considered a module - * @param sourceFile A source file - * @returns A boolean indicating if the file is a module - */ -function isSourceFileAModule(sourceFile: SourceFile): boolean { - return sourceFile.getImportDeclarations().length > 0 || sourceFile.getExportedDeclarations().size > 0; -} - export class TypeScriptToFamixProcessor { private entityDictionary: EntityDictionary; + // TODO: get rid of these maps public methodsAndFunctionsWithId = new SourceFileDataMap(); // Maps the Famix method, constructor, getter, setter and function ids to their ts-morph method, constructor, getter, setter or function object - public accessMap = new SourceFileDataMap(); // Maps the Famix parameter, variable, property and enum value ids to their ts-morph parameter, variable, property or enum member object - // public classes = new SourceFileDataArray(); // Array of all the classes of the source files - // public interfaces = new SourceFileDataArray(); // Array of all the interfaces of the source files - public modules = new SourceFileDataArray(); // Array of all the source files which are modules - public listOfExportMaps = new SourceFileDataArray>(); // Array of all the export maps private processedNodesWithTypeParams = new SourceFileDataSet(); // Set of nodes that have been processed and have type parameters private currentCC: { [key: string]: number }; // Stores the cyclomatic complexity metrics for the current source file @@ -43,28 +30,6 @@ export class TypeScriptToFamixProcessor { this.currentCC = {}; } - private setCurrentSourceFileName(sourceFileName: string): void { - this.methodsAndFunctionsWithId.setSourceFileName(sourceFileName); - this.accessMap.setSourceFileName(sourceFileName); - // this.classes.setSourceFileName(sourceFileName); - // this.interfaces.setSourceFileName(sourceFileName); - this.modules.setSourceFileName(sourceFileName); - this.listOfExportMaps.setSourceFileName(sourceFileName); - this.processedNodesWithTypeParams.setSourceFileName(sourceFileName); - - // this.entityDictionary.setCurrentSourceFileName(sourceFileName); - } - - public removeNodesBySourceFile(sourceFile: string) { - this.methodsAndFunctionsWithId.removeBySourceFileName(sourceFile); - this.accessMap.removeBySourceFileName(sourceFile); - // this.classes.removeBySourceFileName(sourceFile); - // this.interfaces.removeBySourceFileName(sourceFile); - this.modules.removeBySourceFileName(sourceFile); - this.listOfExportMaps.removeBySourceFileName(sourceFile); - this.processedNodesWithTypeParams.removeBySourceFileName(sourceFile); - } - /** * Gets the path of a module to be imported * @param importDecl An import declaration @@ -96,7 +61,6 @@ export class TypeScriptToFamixProcessor { this.currentCC = {}; } - this.setCurrentSourceFileName(file.getFilePath()); this.processFile(file); }); } @@ -105,18 +69,8 @@ export class TypeScriptToFamixProcessor { * Builds a Famix model for a source file * @param f A source file */ - private processFile(f: SourceFile): void { - const isModule = isSourceFileAModule(f); - - if (isModule) { - this.modules.push(f); - } - - const exportMap = f.getExportedDeclarations(); - if (exportMap) this.listOfExportMaps.push(exportMap); - - const fmxFile = this.entityDictionary.createOrGetFamixFile(f, isModule); - + private processFile(f: SourceFile): void { + const fmxFile = this.entityDictionary.createOrGetFamixFile(f); logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`); this.processComments(f, fmxFile); @@ -277,6 +231,7 @@ export class TypeScriptToFamixProcessor { logger.debug(`Finding Modules:`); m.getModules().forEach(md => { const fmxModule = this.processModule(md); + // TODO: need to ensure that there are no duplicates fmxScope.addModule(fmxModule); }); } @@ -308,12 +263,14 @@ export class TypeScriptToFamixProcessor { logger.debug(`Class: ${c.getName()}, (${c.getType().getText()}), fqn = ${fmxClass.fullyQualifiedName}`); + // TODO: need to ensure that there are no duplicates this.processComments(c, fmxClass); this.processDecorators(c, fmxClass); this.processStructuredType(c, fmxClass); + // TODO: need to ensure that there are no duplicates c.getConstructors().forEach(con => { const fmxCon = this.processMethod(con); fmxClass.addMethod(fmxCon); @@ -829,100 +786,52 @@ export class TypeScriptToFamixProcessor { // exports has name -> Declaration -- the declaration can be used to find the FamixElement // handle `import path = require("path")` for example - public processImportClausesForImportEqualsDeclarations(sourceFiles: Array, exports: Array>): void { + public processImportClausesForImportEqualsDeclarations(sourceFiles: Array): void { logger.info(`Creating import clauses from ImportEqualsDeclarations in source files:`); sourceFiles.forEach(sourceFile => { sourceFile.forEachDescendant(node => { if (Node.isImportEqualsDeclaration(node)) { - // You've found an ImportEqualsDeclaration - logger.info("Declaration Name:", node.getName()); - logger.info("Module Reference Text:", node.getModuleReference().getText()); - // what's the name of the imported entity? - // const importedEntity = node.getName(); - // create a famix import clause - const namedImport = node.getNameNode(); - this.entityDictionary.oldCreateOrGetFamixImportClause({ - importDeclaration: node, - importerSourceFile: sourceFile, - moduleSpecifierFilePath: node.getModuleReference().getText(), - importElement: namedImport, - isInExports: exports.find(e => e.has(namedImport.getText())) !== undefined, - isDefaultExport: false - }); - // this.entityDictionary.createFamixImportClause(importedEntity, importingEntity); + // TODO: implement getting all the imports with require (look up to tests for all the cases) + this.entityDictionary.ensureFamixImportClauseForImportEqualsDeclaration(node); } }); - } - ); + }); } /** * Builds a Famix model for the import clauses of the source files which are modules * @param modules An array of modules - * @param exports An array of maps of exported declarations */ - public processImportClausesForModules(modules: Array, exports: Array>): void { + public processImportClausesForModules(modules: Array): void { logger.info(`Creating import clauses from ${modules.length} modules:`); + // TODO: how to handle the reexport? modules.forEach(module => { - const modulePath = module.getFilePath(); + module.getImportDeclarations().forEach(impDecl => { - logger.info(`Importing ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); - const path = this.getModulePath(impDecl); - impDecl.getNamedImports().forEach(namedImport => { - logger.info(`Importing (named) ${namedImport.getName()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); - const importedEntityName = namedImport.getName(); - const importFoundInExports = this.isInExports(exports, importedEntityName); - logger.debug(`Processing ImportSpecifier: ${namedImport.getText()}, pos=${namedImport.getStart()}`); - this.entityDictionary.oldCreateOrGetFamixImportClause({ - importDeclaration: impDecl, - importerSourceFile: module, - moduleSpecifierFilePath: path, - importElement: namedImport, - isInExports: importFoundInExports, - isDefaultExport: false - }); + this.entityDictionary.ensureFamixImportClauseForNamedImport( + impDecl, + namedImport, + module, + ); }); - + const defaultImport = impDecl.getDefaultImport(); if (defaultImport !== undefined) { - logger.info(`Importing (default) ${defaultImport.getText()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); - logger.debug(`Processing Default Import: ${defaultImport.getText()}, pos=${defaultImport.getStart()}`); - this.entityDictionary.oldCreateOrGetFamixImportClause({ - importDeclaration: impDecl, - importerSourceFile: module, - moduleSpecifierFilePath: path, - importElement: defaultImport, - isInExports: false, - isDefaultExport: true - }); + this.entityDictionary.ensureFamixImportClauseForDefaultImport( + defaultImport, + ); } const namespaceImport = impDecl.getNamespaceImport(); if (namespaceImport !== undefined) { - logger.info(`Importing (namespace) ${namespaceImport.getText()} from ${impDecl.getModuleSpecifierValue()} in ${modulePath}`); - this.entityDictionary.oldCreateOrGetFamixImportClause({ - importDeclaration: impDecl, - importerSourceFile: module, - moduleSpecifierFilePath: path, - importElement: namespaceImport, - isInExports: false, - isDefaultExport: false - }); + this.entityDictionary.ensureFamixImportClauseForNamespaceImport( + namespaceImport, + ); } }); }); } - - private isInExports(exports: ReadonlyMap[], importedEntityName: string) { - let importFoundInExports = false; - exports.forEach(e => { - if (e.has(importedEntityName)) { - importFoundInExports = true; - } - }); - return importFoundInExports; - } private processInheritanceForClass(cls: ClassDeclaration) { logger.debug(`Checking class inheritance for ${cls.getName()}`); diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index e4a1aca9..01b9d956 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -14,15 +14,13 @@ import { logger } from "../analyze"; import GraphemeSplitter = require('grapheme-splitter'); import * as Helpers from "./helpers_creation"; import * as FQNFunctions from "../fqn"; -import path from "path"; -import { convertToRelativePath } from "./helpers_path"; import { SourceFileDataMap } from "./SourceFileData"; -import { getFamixIndexFileAnchorFileName } from "./famixIndexFileAnchorHelper"; +import { getFamixIndexFileAnchorFileName } from "../helpers"; import { FullyQualifiedNameEntity } from "../lib/famix/model/interfaces"; import { Node as TsMorphNode } from "ts-morph"; import _ from "lodash"; -import { getInterfaceOrClassDeclarationFromExpression } from "./helpersTsMorphElementsProcessing"; +import { getInterfaceOrClassDeclarationFromExpression, isSourceFileAModule } from "./helpersTsMorphElementsProcessing"; import { EntityWithSourceAnchor } from "../lib/famix/model/famix/sourced_entity"; export type TSMorphObjectType = ImportDeclaration | ImportEqualsDeclaration | SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | TypeParameterDeclaration | Identifier | Decorator | GetAccessorDeclaration | SetAccessorDeclaration | ImportSpecifier | CommentRange | EnumDeclaration | EnumMember | TypeAliasDeclaration | ExpressionWithTypeArguments | TSMorphParametricType; @@ -45,20 +43,15 @@ export class EntityDictionary { private config: EntityDictionaryConfig; private absolutePath: string = ""; public famixRep = new FamixRepository(); + // TODO: get rid of all the maps private fmxAliasMap = new SourceFileDataMap(); // Maps the alias names to their Famix model - // private fmxClassMap = new SourceFileDataMap(); // Maps the fully qualified class names to their Famix model - // private fmxInterfaceMap = new SourceFileDataMap(); // Maps the interface names to their Famix model - private fmxModuleMap = new SourceFileDataMap(); // Maps the namespace names to their Famix model - private fmxFileMap = new SourceFileDataMap(); // Maps the source file names to their Famix model private fmxTypeMap = new SourceFileDataMap(); // Maps the types declarations to their Famix model private fmxPrimitiveTypeMap = new SourceFileDataMap(); // Maps the primitive type names to their Famix model private fmxFunctionAndMethodMap = new SourceFileDataMap; // Maps the function names to their Famix model private fmxArrowFunctionMap = new SourceFileDataMap; // Maps the function names to their Famix model private fmxParameterMap = new SourceFileDataMap(); // Maps the parameters to their Famix model private fmxVariableMap = new SourceFileDataMap(); // Maps the variables to their Famix model - private fmxImportClauseMap = new SourceFileDataMap(); // Maps the import clauses to their Famix model private fmxEnumMap = new SourceFileDataMap(); // Maps the enum names to their Famix model - private fmxInheritanceMap = new SourceFileDataMap(); // Maps the inheritance names to their Famix model public fmxElementObjectMap = new SourceFileDataMap(); public tsMorphElementObjectMap = new SourceFileDataMap(); @@ -222,15 +215,14 @@ export class EntityDictionary { * @param isModule A boolean indicating if the source file is a module * @returns The Famix model of the source file */ - public createOrGetFamixFile(f: SourceFile, isModule: boolean): Famix.ScriptEntity | Famix.Module { - let fmxFile: Famix.ScriptEntity; // | Famix.Module; - - const fileName = f.getBaseName(); - // USE getFQN INSTEAD OF getFilePath HERE ? - // const fullyQualifiedFilename = f.getFilePath(); - const fullyQualifiedFilename = FQNFunctions.getFQN(f, f.getFilePath()); - const foundFileName = this.fmxFileMap.get(fullyQualifiedFilename); - if (!foundFileName) { + public createOrGetFamixFile(f: SourceFile): Famix.ScriptEntity | Famix.Module { + const mapToFamixElement = (f: SourceFile) => { + let fmxFile: Famix.ScriptEntity; // | Famix.Module; + + const fileName = f.getBaseName(); + const isModule = isSourceFileAModule(f); + // TODO: do we need to create a module here instead of ScriptEntity? + // If so - we need to add other properties to the module if (isModule) { fmxFile = new Famix.Module(); } @@ -240,20 +232,12 @@ export class EntityDictionary { fmxFile.name = fileName; fmxFile.numberOfLinesOfText = f.getEndLineNumber() - f.getStartLineNumber(); fmxFile.numberOfCharacters = f.getFullText().length; + return fmxFile; + }; - this.initFQN(f, fmxFile); - - this.makeFamixIndexFileAnchor(f, fmxFile); - - this.fmxFileMap.set(fullyQualifiedFilename, fmxFile); - this.famixRep.addElement(fmxFile); - } - else { - fmxFile = foundFileName; - } - - this.fmxElementObjectMap.set(fmxFile,f); - return fmxFile; + return this.ensureFamixElement( + f, mapToFamixElement + ); } /** @@ -262,31 +246,19 @@ export class EntityDictionary { * @returns The Famix model of the module */ public createOrGetFamixModule(moduleDeclaration: ModuleDeclaration): Famix.Module { - if (this.fmxModuleMap.has(moduleDeclaration)) { - const rModule = this.fmxModuleMap.get(moduleDeclaration); - if (rModule) { - return rModule; - } else { - throw new Error(`Famix module ${moduleDeclaration.getName()} is not found in the module map.`); - } - } - - const fmxModule = new Famix.Module(); - const moduleName = moduleDeclaration.getName(); - fmxModule.name = moduleName; - fmxModule.isAmbient = isAmbient(moduleDeclaration); - fmxModule.isNamespace = isNamespace(moduleDeclaration); - fmxModule.isModule = !fmxModule.isNamespace && !fmxModule.isAmbient; - - this.initFQN(moduleDeclaration, fmxModule); - this.makeFamixIndexFileAnchor(moduleDeclaration, fmxModule); - - this.fmxModuleMap.set(moduleDeclaration, fmxModule); - - this.famixRep.addElement(fmxModule); + const mapToFamixElement = (moduleDeclaration: ModuleDeclaration) => { + const fmxModule = new Famix.Module(); + const moduleName = moduleDeclaration.getName(); + fmxModule.name = moduleName; + fmxModule.isAmbient = isAmbient(moduleDeclaration); + fmxModule.isNamespace = isNamespace(moduleDeclaration); + fmxModule.isModule = !fmxModule.isNamespace && !fmxModule.isAmbient; + return fmxModule; + }; - this.fmxElementObjectMap.set(fmxModule,moduleDeclaration); - return fmxModule; + return this.ensureFamixElement( + moduleDeclaration, mapToFamixElement + ); } /** @@ -1385,118 +1357,73 @@ export class EntityDictionary { } } - public createFamixImportClause(importedEntity: Famix.NamedEntity, importingEntity: Famix.Module) { - const fmxImportClause = new Famix.ImportClause(); - fmxImportClause.importedEntity = importedEntity; - fmxImportClause.importingEntity = importingEntity; - importingEntity.addOutgoingImport(fmxImportClause); - this.famixRep.addElement(fmxImportClause); - } - - /** - * Creates a Famix import clause - * @param importClauseInfo The information needed to create a Famix import clause - * @param importDeclaration The import declaration - * @param importer A source file which is a module - * @param moduleSpecifierFilePath The path of the module where the export declaration is - * @param importElement The imported entity - * @param isInExports A boolean indicating if the imported entity is in the exports - * @param isDefaultExport A boolean indicating if the imported entity is a default export - */ - public oldCreateOrGetFamixImportClause(importClauseInfo: {importDeclaration?: ImportDeclaration | ImportEqualsDeclaration, importerSourceFile: SourceFile, moduleSpecifierFilePath: string, importElement: ImportSpecifier | Identifier, isInExports: boolean, isDefaultExport: boolean}): void { - const {importDeclaration, importerSourceFile: importer, moduleSpecifierFilePath, importElement, isInExports, isDefaultExport} = importClauseInfo; - if (importDeclaration && this.fmxImportClauseMap.has(importDeclaration)) { - const rImportClause = this.fmxImportClauseMap.get(importDeclaration); - if (rImportClause) { - 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; + public ensureFamixImportClauseForNamedImport( + importDeclaration: ImportDeclaration, + namedImport: ImportSpecifier, + importingSourceFile: SourceFile + ) { + const ensureImportedEntity = () => { + let importedEntity: Famix.NamedEntity | undefined; - const absolutePathProject = this.getAbsolutePath(); - - const absolutePath = path.normalize(moduleSpecifierFilePath); - logger.debug(`createFamixImportClause: absolutePath: ${absolutePath}`); - logger.debug(`createFamixImportClause: convertToRelativePath: ${convertToRelativePath(absolutePath, absolutePathProject)}`); - const pathInProject: string = convertToRelativePath(absolutePath, absolutePathProject).replace(/\\/g, "/"); - logger.debug(`createFamixImportClause: pathInProject: ${pathInProject}`); - let pathName = "{" + pathInProject + "}."; - logger.debug(`createFamixImportClause: pathName: ${pathName}`); + const symbol = namedImport.getSymbol(); + const aliasedSymbol = symbol?.getAliasedSymbol(); + const namedEntityDeclaration = aliasedSymbol?.getValueDeclaration(); - 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 (namedEntityDeclaration) { + const importedFullyQualifiedName = FQNFunctions.getFQN(namedEntityDeclaration, this.getAbsolutePath()); + importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importedFullyQualifiedName); } - if (importedEntity === undefined) { - importedEntity = new Famix.NamedEntity(); - importedEntity.name = importedEntityName; - if (!isInExports) { - importedEntity.isStub = true; - } - logger.debug(`Creating named entity ${importedEntityName} for ImportSpecifier ${importElement.getText()}`); - this.initFQN(importElement, importedEntity); - logger.debug(`Assigned FQN to entity: ${importedEntity.fullyQualifiedName}`); - this.makeFamixIndexFileAnchor(importElement, importedEntity); - this.famixRep.addElement(importedEntity); - logger.debug(`Added entity to repository: ${importedEntity.fullyQualifiedName}`); + if (!importedEntity) { + // creating a stub + // TODO: check how do we create the FQN for the import specifier + importedEntity = this.ensureFamixElement(namedImport, () => { + importedEntity = new Famix.NamedEntity(); + // TODO: add other properties + return importedEntity; + }); } - } - else if (importDeclaration instanceof ImportEqualsDeclaration) { - importedEntityName = importDeclaration?.getName(); - pathName = pathName + importedEntityName; - importedEntity = new Famix.StructuralEntity(); - importedEntity.name = importedEntityName; - this.initFQN(importDeclaration, importedEntity); - logger.debug(`Assigned FQN to ImportEquals entity: ${importedEntity.fullyQualifiedName}`); - this.makeFamixIndexFileAnchor(importElement, importedEntity); - const anyType = this.createOrGetFamixType('any', undefined, importDeclaration); - (importedEntity as Famix.StructuralEntity).declaredType = anyType; - } else { - importedEntityName = importElement.getText(); - pathName = pathName + (isDefaultExport ? "defaultExport" : "namespaceExport"); - importedEntity = new Famix.NamedEntity(); - importedEntity.name = importedEntityName; - this.initFQN(importElement, importedEntity); - logger.debug(`Assigned FQN to default/namespace entity: ${importedEntity.fullyQualifiedName}`); - this.makeFamixIndexFileAnchor(importElement, importedEntity); - } - if (!isInExports) { - this.famixRep.addElement(importedEntity); - logger.debug(`Added non-exported entity to repository: ${importedEntity.fullyQualifiedName}`); - } - const importerFullyQualifiedName = FQNFunctions.getFQN(importer, this.getAbsolutePath()); - const fmxImporter = this.famixRep.getFamixEntityByFullyQualifiedName(importerFullyQualifiedName) as Famix.Module; - fmxImportClause.importingEntity = fmxImporter; + return importedEntity; + }; + + const ensureImportingEntity = () => { + const importingFullyQualifiedName = FQNFunctions.getFQN(importingSourceFile, this.getAbsolutePath()); + const importingEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importingFullyQualifiedName); + if (!importingEntity) { + throw new Error(`Famix importer with FQN ${importingFullyQualifiedName} not found.`); + } + return importingEntity; + }; + + const importedEntity = ensureImportedEntity(); + const importingEntity = ensureImportingEntity(); + this.ensureFamixImportClause(importedEntity, importingEntity, importDeclaration); + } + + private ensureFamixImportClause(importedEntity: Famix.NamedEntity, importingEntity: Famix.Module, importDeclaration: ImportDeclaration) { + const fmxImportClause = new Famix.ImportClause(); fmxImportClause.importedEntity = importedEntity; - if (importDeclaration instanceof ImportEqualsDeclaration) { - fmxImportClause.moduleSpecifier = importDeclaration?.getModuleReference().getText() as string; - } else { - fmxImportClause.moduleSpecifier = importDeclaration?.getModuleSpecifierValue() as string; - } - - 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); + fmxImportClause.importingEntity = importingEntity; + fmxImportClause.moduleSpecifier = importDeclaration?.getModuleSpecifierValue(); + + const existingFmxImportClause = this.famixRep.getFamixEntityByFullyQualifiedName(fmxImportClause.fullyQualifiedName); + if (!existingFmxImportClause) { + this.makeFamixIndexFileAnchor(importDeclaration, fmxImportClause); + this.famixRep.addElement(fmxImportClause); } } + public ensureFamixImportClauseForDefaultImport(defaultImport: Identifier) { + throw new Error("Not implemented"); + } + + public ensureFamixImportClauseForNamespaceImport(namespaceImport: Identifier) { + throw new Error("Not implemented"); + } + + public ensureFamixImportClauseForImportEqualsDeclaration(importEqualsDeclaration: ImportEqualsDeclaration) { + throw new Error("Not implemented"); + } + /** * Creates a Famix Arrow Function * @param arrowExpression An Expression @@ -1889,17 +1816,13 @@ export class EntityDictionary { public removeEntitiesBySourceFilePath(sourceFilePath: string) { this.fmxAliasMap.removeBySourceFileName(sourceFilePath); - this.fmxModuleMap.removeBySourceFileName(sourceFilePath); - this.fmxFileMap.removeBySourceFileName(sourceFilePath); this.fmxTypeMap.removeBySourceFileName(sourceFilePath); this.fmxPrimitiveTypeMap.removeBySourceFileName(sourceFilePath); this.fmxFunctionAndMethodMap.removeBySourceFileName(sourceFilePath); this.fmxArrowFunctionMap.removeBySourceFileName(sourceFilePath); this.fmxParameterMap.removeBySourceFileName(sourceFilePath); this.fmxVariableMap.removeBySourceFileName(sourceFilePath); - this.fmxImportClauseMap.removeBySourceFileName(sourceFilePath); this.fmxEnumMap.removeBySourceFileName(sourceFilePath); - this.fmxInheritanceMap.removeBySourceFileName(sourceFilePath); this.fmxElementObjectMap.removeBySourceFileName(sourceFilePath); this.tsMorphElementObjectMap.removeBySourceFileName(sourceFilePath); } diff --git a/src/famix_functions/famixIndexFileAnchorHelper.ts b/src/helpers/famixIndexFileAnchorHelper.ts similarity index 79% rename from src/famix_functions/famixIndexFileAnchorHelper.ts rename to src/helpers/famixIndexFileAnchorHelper.ts index 8fde51cd..b21d573f 100644 --- a/src/famix_functions/famixIndexFileAnchorHelper.ts +++ b/src/helpers/famixIndexFileAnchorHelper.ts @@ -1,4 +1,4 @@ -import { convertToRelativePath } from "./helpers_path"; +import { convertToRelativePath } from "../famix_functions/helpers_path"; import path from "path"; export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePathProject: string) => { @@ -21,4 +21,8 @@ export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePa pathInProject = pathInProject.substring(1); } return pathInProject; +}; + +export const getAbsoluteFileNameFromFamixIndexFileAnchor = (famixIndexFileAnchor: string) => { + return `/${famixIndexFileAnchor}`; }; \ No newline at end of file diff --git a/src/helpers/incrementalUpdateHelper.ts b/src/helpers/incrementalUpdateHelper.ts new file mode 100644 index 00000000..0c431e1e --- /dev/null +++ b/src/helpers/incrementalUpdateHelper.ts @@ -0,0 +1,96 @@ +import { Class } from '../lib/famix/model/famix/class'; +import { FamixBaseElement } from "../lib/famix/famix_base_element"; +import { ImportClause, IndexedFileAnchor, Inheritance, Interface, NamedEntity } from '../lib/famix/model/famix'; +import { EntityWithSourceAnchor } from '../lib/famix/model/famix/sourced_entity'; +import { SourceFileChangeType } from '../analyze'; +import { SourceFile } from 'ts-morph'; +import { getAbsoluteFileNameFromFamixIndexFileAnchor } from './famixIndexFileAnchorHelper'; +import { FamixRepository } from '../lib/famix/famix_repository'; + +export const getSourceFilesToUpdate = ( + dependentAssociations: EntityWithSourceAnchor[], + sourceFileChangeMap: Map, + allSourceFiles: SourceFile[] +) => { + const sourceFilesToEnsureEntities = [ + ...(sourceFileChangeMap.get(SourceFileChangeType.Create) || []), + ...(sourceFileChangeMap.get(SourceFileChangeType.Update) || []), + ]; + + const dependentFileNames = getDependentSourceFileNames(dependentAssociations); + const dependentFileNamesToAdd = Array.from(dependentFileNames) + .map(fileName => getAbsoluteFileNameFromFamixIndexFileAnchor(fileName)) + .filter( + fileName => !Array.from(sourceFileChangeMap.values()) + .flat().some(sourceFile => sourceFile.getFilePath() === fileName)); + + const dependentFiles = allSourceFiles.filter( + sourceFile => dependentFileNamesToAdd.includes(sourceFile.getFilePath()) + ); + + return sourceFilesToEnsureEntities.concat(dependentFiles); +}; + +const getDependentSourceFileNames = (dependentAssociations: EntityWithSourceAnchor[]) => { + const dependentFileNames = new Set(); + + dependentAssociations.forEach(entity => { + // todo: ? sourceAnchor instead of indexedfileAnchor + dependentFileNames.add((entity.sourceAnchor as IndexedFileAnchor).fileName); + }); + + return dependentFileNames; +}; + +export const getDependentAssociations = (entities: FamixBaseElement[]) => { + const dependentAssociations: EntityWithSourceAnchor[] = []; + + entities.forEach(entity => { + dependentAssociations.push(...getDependentAssociationsForEntity(entity)); + }); + + return dependentAssociations; +}; + +const getDependentAssociationsForEntity = (entity: FamixBaseElement) => { + const dependentAssociations: EntityWithSourceAnchor[] = []; + + const addElementFileToSet = (association: EntityWithSourceAnchor) => { + dependentAssociations.push(association); + }; + + if (entity instanceof Class) { + Array.from(entity.subInheritances).forEach(inheritance => { + addElementFileToSet(inheritance); + }); + } else if (entity instanceof Interface) { + Array.from(entity.subInheritances).forEach(inheritance => { + addElementFileToSet(inheritance); + }); + } + + if (entity instanceof NamedEntity) { + Array.from(entity.incomingImports).forEach(importClause => { + addElementFileToSet(importClause); + }); + } + // TODO: add other associations + + return dependentAssociations; +}; + +export const removeDependentAssociations = ( + famixRep: FamixRepository, + dependentAssociations: EntityWithSourceAnchor[]) => { + // NOTE: removing the depending associations because they will be recreated later + famixRep.removeElements(dependentAssociations); + famixRep.removeElements(dependentAssociations.map(x => x.sourceAnchor)); + + dependentAssociations.forEach(association => { + if (association instanceof Inheritance) { + association.subclass.removeSuperInheritance(association); + } else if (association instanceof ImportClause) { + association.importingEntity.incomingImports.delete(association); + } + }); +}; \ No newline at end of file diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 00000000..37be9db6 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './incrementalUpdateHelper'; +export * from './famixIndexFileAnchorHelper'; \ No newline at end of file diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index 1925ed54..d56750b7 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -95,53 +95,17 @@ export class FamixRepository { public removeElements(entities: FamixBaseElement[]): void { for (const entity of entities) { this.elements.delete(entity); - // if (entity instanceof Class) { - // this.famixClasses.delete(entity); - // } else if (entity instanceof Interface) { - // this.famixInterfaces.delete(entity); - // } else - if (entity instanceof Module) { - this.famixModules.delete(entity); - } else if (entity instanceof Variable) { - this.famixVariables.delete(entity); - } else if (entity instanceof Method) { - this.famixMethods.delete(entity); - } else if (entity instanceof FamixFunctionEntity || entity instanceof ArrowFunction) { - this.famixFunctions.delete(entity); - } else if (entity instanceof ScriptEntity || entity instanceof Module) { - this.famixFiles.delete(entity); - } - - // if (entity instanceof Famix.SourcedEntity) { - // this.elements.delete(entity.sourceAnchor); - // } - // TODO: maybe delete smth else? } } public removeRelatedAssociations(entities: FamixBaseElement[]): void { for (const entity of entities) { - // Array.from(this.elements.values()).forEach(e => { - // if (e instanceof Famix.Inheritance && e.subclass === entity) { - // this.elements.delete(e); - // e.subclass.removeSuperInheritance(e); - // e.superclass.removeSubInheritance(e); - // } else if (e instanceof Famix.ImportClause && e.importingEntity === entity) { - // this.elements.delete(e); - // e.importingEntity.removeOutgoingImport(e); - // e.importedEntity.removeIncomingImport(e); - // } else if (e instanceof Famix.Access && e.accessor === entity) { - // this.elements.delete(e); - // e.accessor.removeAccess(e); - // e.variable.removeIncomingAccess(e); - // } else if (e instanceof Famix.Concretisation && e.concreteEntity === entity) { - // this.elements.delete(e); - // } - // }); - if (entity instanceof Famix.Inheritance) { entity.subclass.removeSuperInheritance(entity); entity.superclass.removeSubInheritance(entity); + } else if (entity instanceof Famix.ImportClause) { + entity.importingEntity.removeOutgoingImport(entity); + entity.importedEntity.removeIncomingImport(entity); } // TODO: Add more conditions here for other types of associations } diff --git a/src/lib/famix/model/famix/import_clause.ts b/src/lib/famix/model/famix/import_clause.ts index 054bf0ce..763d5eaf 100644 --- a/src/lib/famix/model/famix/import_clause.ts +++ b/src/lib/famix/model/famix/import_clause.ts @@ -1,9 +1,10 @@ import { FamixJSONExporter } from "../../famix_JSON_exporter"; -import { Entity } from "./entity"; +import { FullyQualifiedNameEntity } from "../interfaces"; import { Module } from "./module"; import { NamedEntity } from "./named_entity"; +import { EntityWithSourceAnchor } from "./sourced_entity"; -export class ImportClause extends Entity { +export class ImportClause extends EntityWithSourceAnchor implements FullyQualifiedNameEntity { private _importingEntity!: Module; private _importedEntity!: NamedEntity; @@ -48,4 +49,8 @@ export class ImportClause extends Entity { set moduleSpecifier(moduleSpecifier: string) { this._moduleSpecifier = moduleSpecifier; } + + get fullyQualifiedName(): string { + return `${this.importingEntity.fullyQualifiedName} -> ${this.importedEntity.fullyQualifiedName}`; + } } diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 36d7b995..5c29626b 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -1,12 +1,20 @@ -import { FamixBaseElement, Inheritance } from "../../src"; +import { FamixBaseElement, ImportClause, Inheritance, Module, NamedEntity } from "../../src"; import { FamixRepository } from "../../src"; import { Class, PrimitiveType } from "../../src"; +const namedEntityCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { + const actualAsNamedEntity = actual as NamedEntity; + const expectedAsNamedEntity = expected as NamedEntity; + + return actualAsNamedEntity.fullyQualifiedName === expectedAsNamedEntity.fullyQualifiedName && + actualAsNamedEntity.incomingImports.size === expectedAsNamedEntity.incomingImports.size; +}; + const classCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { const actualAsClass = actual as Class; const expectedAsClass = expected as Class; - return actualAsClass.fullyQualifiedName === expectedAsClass.fullyQualifiedName && + return namedEntityCompareFunction(actualAsClass, expectedAsClass) && actualAsClass.subInheritances.size === expectedAsClass.subInheritances.size && actualAsClass.superInheritances.size === expectedAsClass.superInheritances.size; // TODO: add more properties to compare @@ -23,8 +31,32 @@ const inheritanceCompareFunction = (actual: FamixBaseElement, expected: FamixBas const actualAsInheritance = actual as Inheritance; const expectedAsInheritance = expected as Inheritance; - return actualAsInheritance.superclass.fullyQualifiedName === expectedAsInheritance.superclass.fullyQualifiedName - && actualAsInheritance.subclass.fullyQualifiedName === expectedAsInheritance.subclass.fullyQualifiedName; + return namedEntityCompareFunction(actualAsInheritance.superclass, expectedAsInheritance.superclass) && + namedEntityCompareFunction(actualAsInheritance.subclass, expectedAsInheritance.subclass) && + actualAsInheritance.superclass.subInheritances.size === expectedAsInheritance.superclass.subInheritances.size && + actualAsInheritance.subclass.superInheritances.size === expectedAsInheritance.subclass.superInheritances.size; +}; + +const importClauseCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { + const actualAsImportClause = actual as ImportClause; + const expectedAsImportClause = expected as ImportClause; + + return actualAsImportClause.fullyQualifiedName === expectedAsImportClause.fullyQualifiedName && + actualAsImportClause.importingEntity.incomingImports.size === expectedAsImportClause.importingEntity.incomingImports.size && + actualAsImportClause.importedEntity.incomingImports.size === expectedAsImportClause.importedEntity.incomingImports.size; +}; + +const moduleCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { + const actualAsModule = actual as Module; + const expectedAsModule = expected as Module; + + return namedEntityCompareFunction(actualAsModule, expectedAsModule) && + actualAsModule.isAmbient === expectedAsModule.isAmbient && + actualAsModule.isNamespace === expectedAsModule.isNamespace && + actualAsModule.isModule === expectedAsModule.isModule && + // TODO: do we use the module correctly (inside the createFamixFile method?) + actualAsModule.parentScope?.fullyQualifiedName === expectedAsModule.parentScope?.fullyQualifiedName && + actualAsModule.incomingImports.size === expectedAsModule.incomingImports.size; }; export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, expected: FamixRepository) => { @@ -46,6 +78,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Enum"); expectElementsToBeEqualSize(actual, expected, "Function"); expectElementsToBeEqualSize(actual, expected, "ImportClause"); + expectElementsToBeSame(actual, expected, "ImportClause", importClauseCompareFunction); // expectElementsToBeEqualSize(actual, expected, "IndexedFileAnchor"); expectElementsToBeEqualSize(actual, expected, "Inheritance"); expectElementsToBeSame(actual, expected, "Inheritance", inheritanceCompareFunction); @@ -53,7 +86,9 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Invocation"); expectElementsToBeEqualSize(actual, expected, "Method"); expectElementsToBeEqualSize(actual, expected, "Module"); + expectElementsToBeSame(actual, expected, "Module", moduleCompareFunction); expectElementsToBeEqualSize(actual, expected, "NamedEntity"); + expectElementsToBeSame(actual, expected, "NamedEntity", namedEntityCompareFunction); expectElementsToBeEqualSize(actual, expected, "ParameterConcretisation"); expectElementsToBeEqualSize(actual, expected, "ParameterType"); expectElementsToBeEqualSize(actual, expected, "Parameter"); @@ -67,7 +102,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Property"); expectElementsToBeEqualSize(actual, expected, "Reference"); expectElementsToBeEqualSize(actual, expected, "ScopingEntity"); - // expectElementsToBeEqualSize(actual, expected, "ScriptEntity"); + expectElementsToBeEqualSize(actual, expected, "ScriptEntity"); expectElementsToBeEqualSize(actual, expected, "SourceAnchor"); expectElementsToBeEqualSize(actual, expected, "SourceLanguage"); expectElementsToBeEqualSize(actual, expected, "SourcedEntity"); From 677b46ed9329b4d189dce01be85c4584894170fb Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:23:46 -0400 Subject: [PATCH 14/25] Add reexport tests --- test/importClauseNamedImport.test.ts | 113 ++++++++- .../importClauseNamedImport.test.ts | 2 +- .../associations/importClauseReExport.test.ts | 226 +++++++++++++++++- 3 files changed, 332 insertions(+), 9 deletions(-) diff --git a/test/importClauseNamedImport.test.ts b/test/importClauseNamedImport.test.ts index 74372b3a..9ddc0ede 100644 --- a/test/importClauseNamedImport.test.ts +++ b/test/importClauseNamedImport.test.ts @@ -1,4 +1,4 @@ -import { Class, ImportClause, Module, NamedEntity } from '../src'; +import { Class, ImportClause, Interface, Module, NamedEntity } from '../src'; import { Importer } from '../src/analyze'; import { createProject } from './testUtils'; @@ -146,7 +146,7 @@ describe('Import Clause Named Imports', () => { const fmxRep = importer.famixRepFromProject(project); const NUMBER_OF_MODULES = 3; - const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 2; const NUMBER_OF_NAMED_ENTITIES = 0; const NUMBER_OF_CLASSES = 1; @@ -161,4 +161,113 @@ describe('Import Clause Named Imports', () => { expect(classesList.length).toBe(NUMBER_OF_CLASSES); expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); }); + + it('should handle a 5-file re-export chain', () => { + const originalExportFileName = 'originalExport.ts'; + const reexport1FileName = 'reexport1.ts'; + const reexport2FileName = 'reexport2.ts'; + const reexport3FileName = 'reexport3.ts'; + const finalImportFileName = 'finalImport.ts'; + + const originalExportCode = ` + export interface Interface1 {} + `; + + const reexport1Code = ` + export { Interface1 } from './${originalExportFileName}'; + `; + + const reexport2Code = ` + export { Interface1 } from './${reexport1FileName}'; + `; + + const reexport3Code = ` + export { Interface1 } from './${reexport2FileName}'; + `; + + const finalImportCode = ` + import { Interface1 } from './${reexport3FileName}'; + + class Consumer implements Interface1 { } + `; + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile(originalExportFileName, originalExportCode); + project.createSourceFile(reexport1FileName, reexport1Code); + project.createSourceFile(reexport2FileName, reexport2Code); + project.createSourceFile(reexport3FileName, reexport3Code); + project.createSourceFile(finalImportFileName, finalImportCode); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 5; + const NUMBER_OF_IMPORT_CLAUSES = 4; + const NUMBER_OF_NAMED_ENTITIES = 0; + const NUMBER_OF_INTERFACES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(interfacesList[0].fullyQualifiedName); + }); + + it('should handle a 5-file re-export broken chain', () => { + const originalExportFileName = 'originalExport.ts'; + const reexport1FileName = 'reexport1.ts'; + const reexport2FileName = 'reexport2.ts'; + const reexport3FileName = 'reexport3.ts'; + const finalImportFileName = 'finalImport.ts'; + + const originalExportCode = ` + export interface Interface1 {} + `; + + const reexport1Code = ` + export { Interface1 } from './${originalExportFileName}'; + `; + + // Chain is broken here + const reexport2Code = ``; + + const reexport3Code = ` + export { Interface1 } from './${reexport2FileName}'; + `; + + const finalImportCode = ` + import { Interface1 } from './${reexport3FileName}'; + `; + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile(originalExportFileName, originalExportCode); + project.createSourceFile(reexport1FileName, reexport1Code); + project.createSourceFile(reexport2FileName, reexport2Code); + project.createSourceFile(reexport3FileName, reexport3Code); + project.createSourceFile(finalImportFileName, finalImportCode); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 4; + const NUMBER_OF_IMPORT_CLAUSES = 3; + const NUMBER_OF_NAMED_ENTITIES = 2; + const NUMBER_OF_INTERFACES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const namedEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(namedEntityList.length).toBe(NUMBER_OF_NAMED_ENTITIES); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); + expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(interfacesList[0].fullyQualifiedName); + }); }); diff --git a/test/incremental-update/associations/importClauseNamedImport.test.ts b/test/incremental-update/associations/importClauseNamedImport.test.ts index 9f75208c..710ec15f 100644 --- a/test/incremental-update/associations/importClauseNamedImport.test.ts +++ b/test/incremental-update/associations/importClauseNamedImport.test.ts @@ -6,7 +6,7 @@ const exportSourceFileName = 'exportSourceCode.ts'; const importSourceFileName = 'importSourceCode.ts'; const existingClassName = 'ExistingClass'; -describe('Add new classes to a single file', () => { +describe('Change import clause between 2 files', () => { const sourceCodeWithExport = ` export class ${existingClassName} { } `; diff --git a/test/incremental-update/associations/importClauseReExport.test.ts b/test/incremental-update/associations/importClauseReExport.test.ts index 584983d8..1c8da181 100644 --- a/test/incremental-update/associations/importClauseReExport.test.ts +++ b/test/incremental-update/associations/importClauseReExport.test.ts @@ -2,12 +2,12 @@ import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpec import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; -const exportSourceFileName = 'exportSource.ts'; -const reexportSourceFileName = 'reexportSource.ts'; -const importSourceFileName = 'importSource.ts'; -const existingClassName = 'ExistingClass'; - -describe('Re-export functionality with inheritance changes', () => { +describe('Named import re-export functionality with inheritance changes', () => { + const exportSourceFileName = 'exportSource.ts'; + const reexportSourceFileName = 'reexportSource.ts'; + const importSourceFileName = 'importSource.ts'; + const existingClassName = 'ExistingClass'; + const initialExportCode = ` export class ${existingClassName} { } `; @@ -145,3 +145,217 @@ describe('Re-export functionality with inheritance changes', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); }); + +describe('5-file named import re-export chain test', () => { + const originalExportFileName = 'originalExport.ts'; + const reexport1FileName = 'reexport1.ts'; + const reexport2FileName = 'reexport2.ts'; + const reexport3FileName = 'reexport3.ts'; + const finalImportFileName = 'finalImport.ts'; + + it('should handle changes in the middle of a 5-file re-export chain', () => { + const originalExportCode = ` + export interface Interface1 {} + `; + + const reexport1Code = ` + export { Interface1 } from './${originalExportFileName}'; + `; + + const reexport2Code = ` + export { Interface1 } from './${reexport1FileName}'; + `; + + const reexport3Code = ` + export { Interface1 } from './${reexport2FileName}'; + `; + + const finalImportCode = ` + import { Interface1 } from './${reexport3FileName}'; + + class Consumer implements Interface1 { } + `; + + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(originalExportFileName, originalExportCode) + .addSourceFile(reexport1FileName, reexport1Code) + .addSourceFile(reexport2FileName, reexport2Code) + .addSourceFile(reexport3FileName, reexport3Code) + .addSourceFile(finalImportFileName, finalImportCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the middle file to add an additional class export + const sourceFile = testProjectBuilder.changeSourceFile(reexport1FileName, ''); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [originalExportFileName, originalExportCode], + [reexport1FileName, ''], + [reexport2FileName, reexport2Code], + [reexport3FileName, reexport3Code], + [finalImportFileName, finalImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should handle changes in the middle of a 5-file re-export chain', () => { + const originalExportCode = ` + export interface Interface1 {} + `; + + const reexport1Code = ` + export { Interface1 } from './${originalExportFileName}'; + `; + + const reexport2Code = ` + export { Interface1 } from './${reexport1FileName}'; + `; + + const reexport3Code = ` + export { Interface1 } from './${reexport2FileName}'; + `; + + const finalImportCode = ` + import { Interface1 } from './${reexport3FileName}'; + + class Consumer implements Interface1 { } + `; + + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(originalExportFileName, originalExportCode) + .addSourceFile(reexport1FileName, '') + .addSourceFile(reexport2FileName, reexport2Code) + .addSourceFile(reexport3FileName, reexport3Code) + .addSourceFile(finalImportFileName, finalImportCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the middle file to add an additional class export + const sourceFile = testProjectBuilder.changeSourceFile(reexport1FileName, reexport1Code); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [originalExportFileName, originalExportCode], + [reexport1FileName, reexport1Code], + [reexport2FileName, reexport2Code], + [reexport3FileName, reexport3Code], + [finalImportFileName, finalImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); + +describe('Default named import re-export functionality', () => { + const sourceFileName = 'source.ts'; + const reexportFileName = 'reexport.ts'; + const importFileName = 'import.ts'; + + it('should handle default re-export with alias: export { default as DefaultExport } from "bar.js"', () => { + // arrange + const sourceCode = ` + export default class DefaultClass { } + `; + + const reexportCode = ` + export { default as DefaultExport } from './${sourceFileName}'; + `; + + const importCode = ` + import { DefaultExport } from './${reexportFileName}'; + `; + + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(sourceFileName, sourceCode) + .addSourceFile(reexportFileName, reexportCode) + .addSourceFile(importFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the source to add a property + const modifiedSourceCode = ` + class BaseClass { } + export default class DefaultClass extends BaseClass { } + `; + + const sourceFile = testProjectBuilder.changeSourceFile(sourceFileName, modifiedSourceCode); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [sourceFileName, modifiedSourceCode], + [reexportFileName, reexportCode], + [importFileName, importCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + + it('should handle chained default re-exports with aliases', () => { + // arrange + const originalFileName = 'original.ts'; + const reexport1FileName = 'reexport1.ts'; + const reexport2FileName = 'reexport2.ts'; + const finalImportFileName = 'finalImport.ts'; + + const originalCode = ` + export default interface OriginalInterface { } + `; + + const reexport1Code = ` + export { default as AliasedInterface } from './${originalFileName}'; + `; + + const reexport2Code = ` + export { AliasedInterface as FinalInterface } from './${reexport1FileName}'; + `; + + const finalImportCode = ` + import { FinalInterface } from './${reexport2FileName}'; + + class Implementation implements FinalInterface { } + `; + + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(originalFileName, originalCode) + .addSourceFile(reexport1FileName, reexport1Code) + .addSourceFile(reexport2FileName, reexport2Code) + .addSourceFile(finalImportFileName, finalImportCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the original interface to add a property + const modifiedOriginalCode = ` + interface BaseInterface { } + export default interface OriginalInterface extends BaseInterface { } + `; + + const sourceFile = testProjectBuilder.changeSourceFile(originalFileName, modifiedOriginalCode); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [originalFileName, modifiedOriginalCode], + [reexport1FileName, reexport1Code], + [reexport2FileName, reexport2Code], + [finalImportFileName, finalImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); From 958a03ecf4ac2039b81af72e5e835a1ced95036a Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:03:45 -0400 Subject: [PATCH 15/25] Add namespace import tests --- test/importClauseNamespaceImport.test.ts | 171 ++++++++++- .../importClauseNamespaceImport.test.ts | 125 ++++++++ .../associations/importClauseReExport.test.ts | 288 +++++++++++++++--- 3 files changed, 529 insertions(+), 55 deletions(-) create mode 100644 test/incremental-update/associations/importClauseNamespaceImport.test.ts diff --git a/test/importClauseNamespaceImport.test.ts b/test/importClauseNamespaceImport.test.ts index 549a6c05..a82357fd 100644 --- a/test/importClauseNamespaceImport.test.ts +++ b/test/importClauseNamespaceImport.test.ts @@ -1,4 +1,4 @@ -import { Class, ImportClause, Module, StructuralEntity } from '../src'; +import { Class, ImportClause, Interface, Module, NamedEntity, StructuralEntity } from '../src'; import { Importer } from '../src/analyze'; import { createProject } from './testUtils'; @@ -24,25 +24,24 @@ describe('Import Clause Namespace Imports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); expect(classesList.length).toBe(NUMBER_OF_CLASSES); - expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); }); - it("should work with namespace imports from exported namespace", () => { + it("should work with namespace imports when exported class and interface", () => { const importer = new Importer(); const project = createProject(); project.createSourceFile("/exportingFile.ts", - `export namespace Utils { - export class Helper {} - } + `export class Helper {} + export interface Utils {} `); project.createSourceFile("/importingFile.ts", @@ -52,16 +51,22 @@ describe('Import Clause Namespace Imports', () => { const fmxRep = importer.famixRepFromProject(project); const NUMBER_OF_MODULES = 2; - const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 2; const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + const NUMBER_OF_INTERFACES = 1; const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + const interfaceList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(interfaceList.length).toBe(NUMBER_OF_INTERFACES); expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); }); @@ -79,14 +84,14 @@ describe('Import Clause Namespace Imports', () => { const fmxRep = importer.famixRepFromProject(project); - const NUMBER_OF_MODULES = 2; + const NUMBER_OF_MODULES = 1; const NUMBER_OF_IMPORT_CLAUSES = 1; const NUMBER_OF_STUB_ENTITIES = 1; const NUMBER_OF_CLASSES = 1; const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -96,13 +101,52 @@ describe('Import Clause Namespace Imports', () => { expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); }); - it("should work with namespace imports for re-exports", () => { + it("should work with namespace import and named re-exports", () => { const importer = new Importer(); const project = createProject(); project.createSourceFile("/baseModule.ts", `export class BaseClass {} - export function baseFunction() {} + export interface BaseInterface {} + `); + + project.createSourceFile("/exportingFile.ts", + `export { BaseClass } from './baseModule'; + export { BaseInterface } from './baseModule'; + `); + + project.createSourceFile("/importingFile.ts", + `import * as AllUtils from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 3; + const NUMBER_OF_IMPORT_CLAUSES = 4; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + const NUMBER_OF_INTERFACES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); + }); + + it("should work with 3 files chain namespace re-exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/baseModule.ts", + `export class BaseClass {} + export interface BaseInterface {} `); project.createSourceFile("/exportingFile.ts", @@ -117,19 +161,114 @@ describe('Import Clause Namespace Imports', () => { const fmxRep = importer.famixRepFromProject(project); const NUMBER_OF_MODULES = 3; - const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 5; const NUMBER_OF_STUB_ENTITIES = 0; const NUMBER_OF_CLASSES = 2; + const NUMBER_OF_INTERFACES = 1; const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); expect(classesList.length).toBe(NUMBER_OF_CLASSES); - expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); + }); + + it("should work with 5 files chain namespace re-exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/baseModule.ts", + `export class BaseClass {} + export interface BaseInterface {} + `); + + project.createSourceFile("/exportingFile1.ts", + `export * from './baseModule'; + export class AdditionalClass {} + `); + + project.createSourceFile("/exportingFile2.ts", + `export * from './exportingFile1'; + `); + + project.createSourceFile("/exportingFile3.ts", + `export * from './exportingFile2'; + `); + + project.createSourceFile("/importingFile.ts", + `import * as AllUtils from './exportingFile3'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 5; + const NUMBER_OF_IMPORT_CLAUSES = 11; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 2; + const NUMBER_OF_INTERFACES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); + }); + + + it("should work with 5 files broken chain namespace re-exports", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/baseModule.ts", + `export class BaseClass {} + export interface BaseInterface {} + `); + + project.createSourceFile("/exportingFile1.ts", + `export * from './baseModule'; + export class AdditionalClass {} + `); + + project.createSourceFile("/exportingFile2.ts", ``); + + project.createSourceFile("/exportingFile3.ts", + `export * from './exportingFile2'; + `); + + project.createSourceFile("/importingFile.ts", + `import * as AllUtils from './exportingFile3'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 4; + const NUMBER_OF_IMPORT_CLAUSES = 4; + const NUMBER_OF_STUB_ENTITIES = 2; + const NUMBER_OF_CLASSES = 2; + const NUMBER_OF_INTERFACES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + const interfacesList = Array.from(fmxRep._getAllEntitiesWithType('Interface')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(interfacesList.length).toBe(NUMBER_OF_INTERFACES); }); }); \ No newline at end of file diff --git a/test/incremental-update/associations/importClauseNamespaceImport.test.ts b/test/incremental-update/associations/importClauseNamespaceImport.test.ts new file mode 100644 index 00000000..f45d8a2a --- /dev/null +++ b/test/incremental-update/associations/importClauseNamespaceImport.test.ts @@ -0,0 +1,125 @@ +import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; +import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; + +const exportSourceFileName = 'exportSourceCode.ts'; +const importSourceFileName = 'importSourceCode.ts'; +const existingClassName = 'ExistingClass'; + +describe('Incremental update should work for namespace imports', () => { + const sourceCodeWithExport = ` + export class ${existingClassName} { } + `; + + const sourceCodeWithExportChanged = ` + class NewBaseClass { } + export class ${existingClassName} extends NewBaseClass { } + `; + + const sourceCodeWithoutImport = ` + class NewClass { } + `; + + const sourceCodeWithImport = ` + import * from './${exportSourceFileName}'; + + class NewClass { } + `; + + const sourceCodeWithImportChanged = ` + import * as x from './${exportSourceFileName}'; + + class NewClassChanged { } + `; + + it('should add new import clause association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithoutImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImport); + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should remove an import clause association', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithoutImport); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithoutImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import clause association when export file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, sourceCodeWithExportChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExportChanged], + [importSourceFileName, sourceCodeWithImport] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should retain an import clause association when importing file is changed', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, sourceCodeWithExport) + .addSourceFile(importSourceFileName, sourceCodeWithImport); + + const { importer, famixRep } = testProjectBuilder.build(); + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, sourceCodeWithImportChanged); + + // act + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, sourceCodeWithExport], + [importSourceFileName, sourceCodeWithImportChanged] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); \ No newline at end of file diff --git a/test/incremental-update/associations/importClauseReExport.test.ts b/test/incremental-update/associations/importClauseReExport.test.ts index 1c8da181..1650a414 100644 --- a/test/incremental-update/associations/importClauseReExport.test.ts +++ b/test/incremental-update/associations/importClauseReExport.test.ts @@ -153,28 +153,28 @@ describe('5-file named import re-export chain test', () => { const reexport3FileName = 'reexport3.ts'; const finalImportFileName = 'finalImport.ts'; - it('should handle changes in the middle of a 5-file re-export chain', () => { - const originalExportCode = ` - export interface Interface1 {} - `; + const originalExportCode = ` + export interface Interface1 {} + `; - const reexport1Code = ` - export { Interface1 } from './${originalExportFileName}'; - `; + const reexport1Code = ` + export { Interface1 } from './${originalExportFileName}'; + `; - const reexport2Code = ` - export { Interface1 } from './${reexport1FileName}'; - `; + const reexport2Code = ` + export { Interface1 } from './${reexport1FileName}'; + `; - const reexport3Code = ` - export { Interface1 } from './${reexport2FileName}'; - `; + const reexport3Code = ` + export { Interface1 } from './${reexport2FileName}'; + `; - const finalImportCode = ` - import { Interface1 } from './${reexport3FileName}'; + const finalImportCode = ` + import { Interface1 } from './${reexport3FileName}'; - class Consumer implements Interface1 { } - `; + class Consumer implements Interface1 { } + `; + it('should handle changes in the middle of a 5-file re-export chain', () => { // arrange const testProjectBuilder = new IncrementalUpdateProjectBuilder(); @@ -205,28 +205,6 @@ describe('5-file named import re-export chain test', () => { }); it('should handle changes in the middle of a 5-file re-export chain', () => { - const originalExportCode = ` - export interface Interface1 {} - `; - - const reexport1Code = ` - export { Interface1 } from './${originalExportFileName}'; - `; - - const reexport2Code = ` - export { Interface1 } from './${reexport1FileName}'; - `; - - const reexport3Code = ` - export { Interface1 } from './${reexport2FileName}'; - `; - - const finalImportCode = ` - import { Interface1 } from './${reexport3FileName}'; - - class Consumer implements Interface1 { } - `; - // arrange const testProjectBuilder = new IncrementalUpdateProjectBuilder(); testProjectBuilder @@ -359,3 +337,235 @@ describe('Default named import re-export functionality', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); }); + +describe('Namespace import re-export with inheritance changes', () => { + const exportSourceFileName = 'exportSource.ts'; + const reexportSourceFileName = 'reexportSource.ts'; + const importSourceFileName = 'importSource.ts'; + const existingClassName = 'ExistingClass'; + + const initialExportCode = ` + export class ${existingClassName} { } + `; + + const exportCodeWithInheritance = ` + class BaseClass { } + + export class ${existingClassName} extends BaseClass { } + `; + + const reexportCode = ` + export * from './${exportSourceFileName}'; + `; + + const importCode = ` + import * as base from './${reexportSourceFileName}'; + + class ConsumerClass extends base.${existingClassName} { } + `; + + it('should maintain re-export associations when original export adds inheritance', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - change the original export file to add inheritance + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, exportCodeWithInheritance); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, exportCodeWithInheritance], + [reexportSourceFileName, reexportCode], + [importSourceFileName, importCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should establish correct re-export chain from scratch', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, '') + .addSourceFile(importSourceFileName, ''); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - add re-export + let sourceFile = testProjectBuilder.changeSourceFile(reexportSourceFileName, reexportCode); + let fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // act - add import from re-export + sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, importCode); + fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, reexportCode], + [importSourceFileName, importCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should handle removing re-export while maintaining original export', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - remove re-export + const sourceFile = testProjectBuilder.changeSourceFile(reexportSourceFileName, ''); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, ''], + [importSourceFileName, importCode] + ]); + + // assert + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should update re-export associations when import file changes to use original export', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - change import to use original export instead of re-export + const directImportCode = ` + import { ${existingClassName} } from './${exportSourceFileName}'; + + class ConsumerClass { + private instance: ${existingClassName}; + + constructor() { + this.instance = new ${existingClassName}(); + } + } + `; + + const sourceFile = testProjectBuilder.changeSourceFile(importSourceFileName, directImportCode); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, initialExportCode], + [reexportSourceFileName, reexportCode], + [importSourceFileName, directImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); + +describe('5-file namespace import re-export chain test', () => { + const originalExportFileName = 'originalExport.ts'; + const reexport1FileName = 'reexport1.ts'; + const reexport2FileName = 'reexport2.ts'; + const reexport3FileName = 'reexport3.ts'; + const finalImportFileName = 'finalImport.ts'; + + const originalExportCode = ` + export interface Interface1 {} + `; + + const reexport1Code = ` + export * from './${originalExportFileName}'; + `; + + const reexport2Code = ` + export * from './${reexport1FileName}'; + `; + + const reexport3Code = ` + export * from './${reexport2FileName}'; + `; + + const finalImportCode = ` + import * as x from './${reexport3FileName}'; + + class Consumer implements x.Interface1 { } + `; + it('should handle changes in the middle of a 5-file re-export chain', () => { + + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(originalExportFileName, originalExportCode) + .addSourceFile(reexport1FileName, reexport1Code) + .addSourceFile(reexport2FileName, reexport2Code) + .addSourceFile(reexport3FileName, reexport3Code) + .addSourceFile(finalImportFileName, finalImportCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the middle file to add an additional class export + const sourceFile = testProjectBuilder.changeSourceFile(reexport1FileName, ''); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [originalExportFileName, originalExportCode], + [reexport1FileName, ''], + [reexport2FileName, reexport2Code], + [reexport3FileName, reexport3Code], + [finalImportFileName, finalImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); + + it('should handle changes in the middle of a 5-file re-export chain', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(originalExportFileName, originalExportCode) + .addSourceFile(reexport1FileName, '') + .addSourceFile(reexport2FileName, reexport2Code) + .addSourceFile(reexport3FileName, reexport3Code) + .addSourceFile(finalImportFileName, finalImportCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - modify the middle file to add an additional class export + const sourceFile = testProjectBuilder.changeSourceFile(reexport1FileName, reexport1Code); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [originalExportFileName, originalExportCode], + [reexport1FileName, reexport1Code], + [reexport2FileName, reexport2Code], + [reexport3FileName, reexport3Code], + [finalImportFileName, finalImportCode] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); +}); \ No newline at end of file From a088c2fa4d3b8d93073654b522ac738c2f327eaf Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:43:19 -0400 Subject: [PATCH 16/25] Add implementation for the Imoprt Clause for - named import/export; - namespace import/export; - reexport. Incapsulate ImportClause creation logic in a separate file --- src/analyze.ts | 9 +- src/analyze_functions/process_functions.ts | 34 ++- src/famix_functions/EntityDictionary.ts | 69 +----- src/famix_functions/ImportClauseCreator.ts | 204 ++++++++++++++++++ .../helpersTsMorphElementsProcessing.ts | 23 +- src/helpers/incrementalUpdateHelper.ts | 23 +- .../transientDependencyResolverHelper.ts | 67 ++++++ src/lib/famix/famix_repository.ts | 7 + 8 files changed, 354 insertions(+), 82 deletions(-) create mode 100644 src/famix_functions/ImportClauseCreator.ts create mode 100644 src/helpers/transientDependencyResolverHelper.ts diff --git a/src/analyze.ts b/src/analyze.ts index 8c8d33ce..b3340169 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -5,7 +5,7 @@ import { Logger } from "tslog"; import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/EntityDictionary"; import path from "path"; import { TypeScriptToFamixProcessor } from "./analyze_functions/process_functions"; -import { getFamixIndexFileAnchorFileName } from "./helpers"; +import { getFamixIndexFileAnchorFileName, getTransientDependentEntities } from "./helpers"; import { isSourceFileAModule } from "./famix_functions/helpersTsMorphElementsProcessing"; import { FamixBaseElement } from "./lib/famix/famix_base_element"; import { getDependentAssociations, getSourceFilesToUpdate, removeDependentAssociations } from "./helpers"; @@ -142,10 +142,13 @@ export class Importer { const allSourceFiles = this.project.getSourceFiles(); const dependentAssociations = getDependentAssociations(removedEntities); + const transientDependentAssociations = getTransientDependentEntities(this.entityDictionary, sourceFileChangeMap); - removeDependentAssociations(this.entityDictionary.famixRep, dependentAssociations); + const associationsToRemove = [...dependentAssociations, ...transientDependentAssociations]; - const sourceFilesToEnsure = getSourceFilesToUpdate(dependentAssociations, sourceFileChangeMap, allSourceFiles); + removeDependentAssociations(this.entityDictionary.famixRep, associationsToRemove); + + const sourceFilesToEnsure = getSourceFilesToUpdate(associationsToRemove, sourceFileChangeMap, allSourceFiles); this.processFunctions.processFiles(sourceFilesToEnsure); const sourceFilesToDelete = sourceFileChangeMap.get(SourceFileChangeType.Delete) || []; diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index d9f15470..abc999f9 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -7,6 +7,7 @@ import { getFQN } from "../fqn"; import { EntityDictionary, InvocableType } from "../famix_functions/EntityDictionary"; import { SourceFileDataMap, SourceFileDataSet } from "../famix_functions/SourceFileData"; import { getClassesDeclaredInArrowFunctions } from "../famix_functions/helpersTsMorphElementsProcessing"; +import { ImportClauseCreator } from "../famix_functions/ImportClauseCreator"; export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember; export type FamixID = number; @@ -17,6 +18,7 @@ type ScopedTypes = Famix.ScriptEntity | Famix.Module | Famix.Function | Famix.Me export class TypeScriptToFamixProcessor { private entityDictionary: EntityDictionary; + private importClauseCreator: ImportClauseCreator; // TODO: get rid of these maps public methodsAndFunctionsWithId = new SourceFileDataMap(); // Maps the Famix method, constructor, getter, setter and function ids to their ts-morph method, constructor, getter, setter or function object @@ -27,6 +29,7 @@ export class TypeScriptToFamixProcessor { constructor(entityDictionary: EntityDictionary) { this.entityDictionary = entityDictionary; + this.importClauseCreator = new ImportClauseCreator(entityDictionary); this.currentCC = {}; } @@ -792,7 +795,7 @@ export class TypeScriptToFamixProcessor { sourceFile.forEachDescendant(node => { if (Node.isImportEqualsDeclaration(node)) { // TODO: implement getting all the imports with require (look up to tests for all the cases) - this.entityDictionary.ensureFamixImportClauseForImportEqualsDeclaration(node); + this.importClauseCreator.ensureFamixImportClauseForImportEqualsDeclaration(node); } }); }); @@ -804,29 +807,44 @@ export class TypeScriptToFamixProcessor { */ public processImportClausesForModules(modules: Array): void { logger.info(`Creating import clauses from ${modules.length} modules:`); - // TODO: how to handle the reexport? modules.forEach(module => { + const exportDeclarations = module.getExportDeclarations(); + const reExports = Array.from(exportDeclarations.entries()) + .flatMap(([, declarations]) => declarations) + .filter(declaration => declaration.hasModuleSpecifier()); + + reExports.forEach(reExport => { + const namedExports = reExport.getNamedExports(); + namedExports.forEach(namedExport => { + this.importClauseCreator.ensureFamixImportClauseForNamedImport( + reExport, namedExport, module, + ); + }); + + if (reExport.isNamespaceExport()) { + this.importClauseCreator.ensureFamixImportClauseForNamespaceExports(reExport, module); + } + }); module.getImportDeclarations().forEach(impDecl => { impDecl.getNamedImports().forEach(namedImport => { - this.entityDictionary.ensureFamixImportClauseForNamedImport( - impDecl, - namedImport, - module, + this.importClauseCreator.ensureFamixImportClauseForNamedImport( + impDecl, namedImport, module, ); }); const defaultImport = impDecl.getDefaultImport(); if (defaultImport !== undefined) { - this.entityDictionary.ensureFamixImportClauseForDefaultImport( + this.importClauseCreator.ensureFamixImportClauseForDefaultImport( defaultImport, ); } const namespaceImport = impDecl.getNamespaceImport(); if (namespaceImport !== undefined) { - this.entityDictionary.ensureFamixImportClauseForNamespaceImport( + this.importClauseCreator.ensureFamixImportClauseForNamespaceImport( namespaceImport, + module ); } }); diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index 01b9d956..01f801cb 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -323,7 +323,7 @@ export class EntityDictionary { ); } - private ensureFamixElement< + public ensureFamixElement< TTMorphNode extends Node, TFamixElement extends Famix.SourcedEntity>( node: TTMorphNode, @@ -1357,73 +1357,6 @@ export class EntityDictionary { } } - public ensureFamixImportClauseForNamedImport( - importDeclaration: ImportDeclaration, - namedImport: ImportSpecifier, - importingSourceFile: SourceFile - ) { - const ensureImportedEntity = () => { - let importedEntity: Famix.NamedEntity | undefined; - - const symbol = namedImport.getSymbol(); - const aliasedSymbol = symbol?.getAliasedSymbol(); - const namedEntityDeclaration = aliasedSymbol?.getValueDeclaration(); - - if (namedEntityDeclaration) { - const importedFullyQualifiedName = FQNFunctions.getFQN(namedEntityDeclaration, this.getAbsolutePath()); - importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importedFullyQualifiedName); - } - if (!importedEntity) { - // creating a stub - // TODO: check how do we create the FQN for the import specifier - importedEntity = this.ensureFamixElement(namedImport, () => { - importedEntity = new Famix.NamedEntity(); - // TODO: add other properties - return importedEntity; - }); - } - return importedEntity; - }; - - const ensureImportingEntity = () => { - const importingFullyQualifiedName = FQNFunctions.getFQN(importingSourceFile, this.getAbsolutePath()); - const importingEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importingFullyQualifiedName); - if (!importingEntity) { - throw new Error(`Famix importer with FQN ${importingFullyQualifiedName} not found.`); - } - return importingEntity; - }; - - const importedEntity = ensureImportedEntity(); - const importingEntity = ensureImportingEntity(); - this.ensureFamixImportClause(importedEntity, importingEntity, importDeclaration); - } - - private ensureFamixImportClause(importedEntity: Famix.NamedEntity, importingEntity: Famix.Module, importDeclaration: ImportDeclaration) { - const fmxImportClause = new Famix.ImportClause(); - fmxImportClause.importedEntity = importedEntity; - fmxImportClause.importingEntity = importingEntity; - fmxImportClause.moduleSpecifier = importDeclaration?.getModuleSpecifierValue(); - - const existingFmxImportClause = this.famixRep.getFamixEntityByFullyQualifiedName(fmxImportClause.fullyQualifiedName); - if (!existingFmxImportClause) { - this.makeFamixIndexFileAnchor(importDeclaration, fmxImportClause); - this.famixRep.addElement(fmxImportClause); - } - } - - public ensureFamixImportClauseForDefaultImport(defaultImport: Identifier) { - throw new Error("Not implemented"); - } - - public ensureFamixImportClauseForNamespaceImport(namespaceImport: Identifier) { - throw new Error("Not implemented"); - } - - public ensureFamixImportClauseForImportEqualsDeclaration(importEqualsDeclaration: ImportEqualsDeclaration) { - throw new Error("Not implemented"); - } - /** * Creates a Famix Arrow Function * @param arrowExpression An Expression diff --git a/src/famix_functions/ImportClauseCreator.ts b/src/famix_functions/ImportClauseCreator.ts new file mode 100644 index 00000000..328992fd --- /dev/null +++ b/src/famix_functions/ImportClauseCreator.ts @@ -0,0 +1,204 @@ +import { FamixRepository } from "../lib/famix/famix_repository"; +import { EntityDictionary, TSMorphObjectType } from "./EntityDictionary"; +import { ExportDeclaration, ExportSpecifier, ImportDeclaration, ImportSpecifier, SourceFile, Node, ts, Identifier, Symbol, ImportEqualsDeclaration } from "ts-morph"; +import { getDeclarationFromImportOrExport, getDeclarationFromSymbol } from "./helpersTsMorphElementsProcessing"; +import { getFamixIndexFileAnchorFileName } from "../helpers"; +import * as Famix from "../lib/famix/model/famix"; +import * as FQNFunctions from "../fqn"; + +export class ImportClauseCreator { + private entityDictionary: EntityDictionary; + private famixRep: FamixRepository; + + constructor(entityDictionary: EntityDictionary) { + this.entityDictionary = entityDictionary; + this.famixRep = entityDictionary.famixRep; + } + + public ensureFamixImportClauseForNamedImport( + importDeclaration: ImportDeclaration | ExportDeclaration, + namedImport: ImportSpecifier | ExportSpecifier, + importingSourceFile: SourceFile + ) { + const namedEntityDeclaration = getDeclarationFromImportOrExport(namedImport); + + const importedEntity = this.ensureImportedEntity(namedEntityDeclaration, namedImport); + const importingEntity = this.ensureImportingEntity(importingSourceFile); + const moduleSpecifier = this.getModuleSpecifierFromDeclaration(importDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importDeclaration); + } + + public ensureFamixImportClauseForNamespaceImport(namespaceImport: Identifier, importingSourceFile: SourceFile) { + const localSymbol = namespaceImport.getSymbolOrThrow(); + const moduleSymbol = localSymbol.getAliasedSymbolOrThrow(); + + const moduleSpecifier = this.getModuleSpecifierFromIdentifier(moduleSymbol); + const exportsOfModule = moduleSymbol.getExports(); + + const importingEntity = this.ensureImportingEntity(importingSourceFile); + + this.handleNamespaceImportOrExport(exportsOfModule, importingEntity, moduleSpecifier, namespaceImport); + } + + public ensureFamixImportClauseForNamespaceExports( + exportDeclaration: ExportDeclaration, + exportingFile: SourceFile + ) { + const moduleSpecifierSourceFile = exportDeclaration.getModuleSpecifierSourceFile(); + const moduleSpecifierSymbol = moduleSpecifierSourceFile?.getSymbol(); + + const moduleSpecifier = this.getModuleSpecifierFromDeclaration(exportDeclaration); + + const importingEntity = this.ensureImportingEntity(exportingFile); + + if (moduleSpecifierSymbol) { + const reexportedExports = moduleSpecifierSymbol.getExports(); + this.handleNamespaceImportOrExport(reexportedExports, importingEntity, moduleSpecifier, exportDeclaration); + } else { + const importedEntity = this.ensureImportedEntityStub(exportDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, exportDeclaration); + } + } + + public ensureFamixImportClauseForDefaultImport(defaultImport: Identifier) { + throw new Error("Not implemented"); + } + + public ensureFamixImportClauseForImportEqualsDeclaration(importEqualsDeclaration: ImportEqualsDeclaration) { + throw new Error("Not implemented"); + } + + private ensureImportedEntity = (namedEntityDeclaration: Node | undefined, importedEntityDeclaration: Node) => { + let importedEntity: Famix.NamedEntity | undefined; + + if (namedEntityDeclaration) { + const importedFullyQualifiedName = FQNFunctions.getFQN(namedEntityDeclaration, this.entityDictionary.getAbsolutePath()); + importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importedFullyQualifiedName); + } + if (!importedEntity) { + // TODO: check how do we create the FQN for the import specifier + importedEntity = this.ensureImportedEntityStub(importedEntityDeclaration); + } + return importedEntity; + }; + + private ensureImportingEntity = (importingSourceFile: SourceFile) => { + const importingFullyQualifiedName = FQNFunctions.getFQN(importingSourceFile, this.entityDictionary.getAbsolutePath()); + const importingEntity = this.famixRep.getFamixEntityByFullyQualifiedName(importingFullyQualifiedName); + if (!importingEntity) { + throw new Error(`Famix importer with FQN ${importingFullyQualifiedName} not found.`); + } + return importingEntity; + }; + + private ensureFamixImportClause( + importedEntity: Famix.NamedEntity, + importingEntity: Famix.Module, + moduleSpecifier: string, + importOrExportDeclaration: Node + ) { + const fmxImportClause = new Famix.ImportClause(); + fmxImportClause.importedEntity = importedEntity; + fmxImportClause.importingEntity = importingEntity; + fmxImportClause.moduleSpecifier = moduleSpecifier; + + const existingFmxImportClause = this.famixRep.getFamixEntityByFullyQualifiedName(fmxImportClause.fullyQualifiedName); + if (!existingFmxImportClause) { + this.entityDictionary.makeFamixIndexFileAnchor(importOrExportDeclaration as TSMorphObjectType, fmxImportClause); + this.famixRep.addElement(fmxImportClause); + } + } + + private getModuleSpecifierFromDeclaration(importOrExportDeclaration: ImportDeclaration | ExportDeclaration): string { + return getFamixIndexFileAnchorFileName( + importOrExportDeclaration.getModuleSpecifierValue() ?? '', + this.entityDictionary.getAbsolutePath() + ); + } + + private getModuleSpecifierFromIdentifier(moduleSymbol: Symbol): string { + const moduleSourceFile = moduleSymbol.getValueDeclaration(); + const moduleSourceFileName = moduleSourceFile && (moduleSourceFile instanceof SourceFile) + ? moduleSourceFile.getFilePath() + : ''; + return getFamixIndexFileAnchorFileName(moduleSourceFileName, this.entityDictionary.getAbsolutePath()); + } + + private ensureImportedEntityStub(importOrExportDeclaration: Node) { + return this.entityDictionary.ensureFamixElement, Famix.NamedEntity>(importOrExportDeclaration, () => { + const stub = new Famix.NamedEntity(); + stub.isStub = true; + // TODO: add other properties + return stub; + }); + }; + + /** + * Ensures namespace import or export. + * @param exports All the exports of the exporting file. + * @param importingEntity The entity for the importing module. + * @param moduleSpecifier The name of the exporting file (if re-exports - the name of the first exporting file in the chain). + * @param importOrExportDeclaration The declaration for the import/export. Ex.: "import * as ns from 'module';" + */ + private handleNamespaceImportOrExport( + exports: Symbol[], + importingEntity: Famix.Module, + moduleSpecifier: string, + importOrExportDeclaration: Node + ) { + const exportsOfModuleSet = new Set(exports); + let importedEntity: Famix.NamedEntity; + + // It no exports found - create a stub + if (exportsOfModuleSet.size === 0) { + importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); + return; + } + + const handleExportSpecifier = (exportedDeclaration: ExportSpecifier) => { + const smb = exportedDeclaration.getSymbol(); + const aliasedSmb = smb?.getAliasedSymbol(); + if (aliasedSmb) { + if (!processedExportsOfModuleSet.has(aliasedSmb)) { + exportsOfModuleSet.add(aliasedSmb); + } + } else { // else - it means the re-export chain is broken + importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); + } + }; + + const handleNamespaceExport = (exportedDeclaration: ExportDeclaration) => { + const exportDeclarationModule = exportedDeclaration.getModuleSpecifierSourceFile()?.getSymbol(); + if (exportDeclarationModule) { + const reexportedExports = exportDeclarationModule.getExports(); + reexportedExports.forEach(exp => { + if (!processedExportsOfModuleSet.has(exp)) { + exportsOfModuleSet.add(exp); + } + }); + } else { // else - it means the re-export chain is broken + importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); + } + }; + + const processedExportsOfModuleSet = new Set(); + while (exportsOfModuleSet.size > 0) { + const exportedSymbol = exportsOfModuleSet.values().next().value!; + exportsOfModuleSet.delete(exportedSymbol); + processedExportsOfModuleSet.add(exportedSymbol); + + const exportedDeclaration = getDeclarationFromSymbol(exportedSymbol); + if (Node.isExportSpecifier(exportedDeclaration)) { + handleExportSpecifier(exportedDeclaration); + } else if (Node.isExportDeclaration(exportedDeclaration) && exportedDeclaration.isNamespaceExport()) { + handleNamespaceExport(exportedDeclaration); + } else { + const importedEntity = this.ensureImportedEntity(exportedDeclaration, importOrExportDeclaration); + this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); + } + } + } +} \ No newline at end of file diff --git a/src/famix_functions/helpersTsMorphElementsProcessing.ts b/src/famix_functions/helpersTsMorphElementsProcessing.ts index 7f63a100..9be7c95b 100644 --- a/src/famix_functions/helpersTsMorphElementsProcessing.ts +++ b/src/famix_functions/helpersTsMorphElementsProcessing.ts @@ -1,4 +1,5 @@ -import { ArrowFunction, ClassDeclaration, ExpressionWithTypeArguments, ImportSpecifier, InterfaceDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind } from "ts-morph"; +import { ArrowFunction, ClassDeclaration, ExportSpecifier, ExpressionWithTypeArguments, ImportSpecifier, + InterfaceDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind, ts } from "ts-morph"; import { Symbol as TSMorphSymbol } from "ts-morph"; /** @@ -102,3 +103,23 @@ function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): Inte } return undefined; } + +export const getDeclarationFromImportOrExport = (importOrExport: ImportSpecifier | ExportSpecifier): Node | undefined => { + const symbol = importOrExport.getSymbol(); + const aliasedSymbol = symbol?.getAliasedSymbol(); + + return getDeclarationFromSymbol(aliasedSymbol); +}; + +export const getDeclarationFromSymbol = (symbol: TSMorphSymbol | undefined) => { + let entityDeclaration = symbol?.getValueDeclaration(); + + if (!entityDeclaration) { + const declarations = symbol?.getDeclarations(); + if (declarations && declarations?.length > 0) { + entityDeclaration = declarations[0]; + } + } + + return entityDeclaration; +}; \ No newline at end of file diff --git a/src/helpers/incrementalUpdateHelper.ts b/src/helpers/incrementalUpdateHelper.ts index 0c431e1e..57f47b69 100644 --- a/src/helpers/incrementalUpdateHelper.ts +++ b/src/helpers/incrementalUpdateHelper.ts @@ -4,8 +4,10 @@ import { ImportClause, IndexedFileAnchor, Inheritance, Interface, NamedEntity } import { EntityWithSourceAnchor } from '../lib/famix/model/famix/sourced_entity'; import { SourceFileChangeType } from '../analyze'; import { SourceFile } from 'ts-morph'; -import { getAbsoluteFileNameFromFamixIndexFileAnchor } from './famixIndexFileAnchorHelper'; +import { getAbsoluteFileNameFromFamixIndexFileAnchor, getFamixIndexFileAnchorFileName } from './famixIndexFileAnchorHelper'; import { FamixRepository } from '../lib/famix/famix_repository'; +import { EntityDictionary } from 'src/famix_functions/EntityDictionary'; +import { getTransientDependentAssociations } from './transientDependencyResolverHelper'; export const getSourceFilesToUpdate = ( dependentAssociations: EntityWithSourceAnchor[], @@ -52,6 +54,21 @@ export const getDependentAssociations = (entities: FamixBaseElement[]) => { return dependentAssociations; }; +export const getTransientDependentEntities = ( + entityDictionary: EntityDictionary, + sourceFileChangeMap: Map, +) => { + const absoluteProjectPath = entityDictionary.getAbsolutePath(); + + const changedFilesNames = Array.from(sourceFileChangeMap.values()) + .flat() + .map(sourceFile => getFamixIndexFileAnchorFileName(sourceFile.getFilePath(), absoluteProjectPath)); + + const transientDependentAssociations = getTransientDependentAssociations(entityDictionary, changedFilesNames); + + return transientDependentAssociations; +}; + const getDependentAssociationsForEntity = (entity: FamixBaseElement) => { const dependentAssociations: EntityWithSourceAnchor[] = []; @@ -88,9 +105,11 @@ export const removeDependentAssociations = ( dependentAssociations.forEach(association => { if (association instanceof Inheritance) { + association.superclass.removeSubInheritance(association); association.subclass.removeSuperInheritance(association); } else if (association instanceof ImportClause) { - association.importingEntity.incomingImports.delete(association); + association.importedEntity.incomingImports.delete(association); + association.importingEntity.outgoingImports.delete(association); } }); }; \ No newline at end of file diff --git a/src/helpers/transientDependencyResolverHelper.ts b/src/helpers/transientDependencyResolverHelper.ts new file mode 100644 index 00000000..c07dca9c --- /dev/null +++ b/src/helpers/transientDependencyResolverHelper.ts @@ -0,0 +1,67 @@ +import { EntityWithSourceAnchor } from "../lib/famix/model/famix/sourced_entity"; +import { EntityDictionary } from "../famix_functions/EntityDictionary"; +import { Class, ImportClause, IndexedFileAnchor, Interface } from "../lib/famix/model/famix"; + +export const getTransientDependentAssociations = ( + entityDictionary: EntityDictionary, + changedFilesNames: string [] +) => { + const importClauses = entityDictionary.famixRep.getImportClauses(); + + const transientDependentAssociations: Set = new Set(); + + const unprocessedFiles: Set = new Set(changedFilesNames); + const processedFiles: Set = new Set(); + + while (unprocessedFiles.size > 0) { + const file: string = unprocessedFiles.values().next().value!; + unprocessedFiles.delete(file); + processedFiles.add(file); + + importClauses.forEach(importClause => { + if (importClause.moduleSpecifier === file) { + transientDependentAssociations.add(importClause); + if (importClause.importedEntity.isStub) { + transientDependentAssociations.add(importClause.importedEntity); + } + + const importingEntityFileName = (importClause.sourceAnchor as IndexedFileAnchor).fileName; + + if (!unprocessedFiles.has(importingEntityFileName) && !processedFiles.has(importingEntityFileName)) { + unprocessedFiles.add(importingEntityFileName); + } + + getOtherTransientDependencies(entityDictionary, importClause, transientDependentAssociations); + } + }); + } + + return transientDependentAssociations; +}; + +const getOtherTransientDependencies = ( + entityDictionary: EntityDictionary, + importClause: ImportClause, + transientDependentAssociations: Set +) => { + const importedEntity = importClause.importedEntity; + const importingEntityFileName = (importClause.sourceAnchor as IndexedFileAnchor).fileName; + + const inheritances = entityDictionary.famixRep.getInheritances(); + + if (importedEntity instanceof Class || importedEntity instanceof Interface || importedEntity.isStub) { + inheritances.forEach(inheritance => { + const doesInheritanceContainImportedEntity = inheritance.superclass === importClause.importedEntity && + importingEntityFileName === (inheritance.sourceAnchor as IndexedFileAnchor).fileName; + + if (doesInheritanceContainImportedEntity) { + transientDependentAssociations.add(inheritance); + } else if (inheritance.superclass.isStub) { + transientDependentAssociations.add(inheritance); + transientDependentAssociations.add(inheritance.superclass); + } + }); + } + + // TODO: find the other associations between the imported entity and the sourceFile +}; \ No newline at end of file diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index d56750b7..5984b35e 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -111,6 +111,13 @@ export class FamixRepository { } } + public getImportClauses(): Famix.ImportClause[] { + return Array.from(this.elements.values()).filter(e => e instanceof Famix.ImportClause) as Famix.ImportClause[]; + } + + public getInheritances(): Famix.Inheritance[] { + return Array.from(this.elements.values()).filter(e => e instanceof Famix.Inheritance) as Famix.Inheritance[]; + } // Only for tests From 26ab8b65ca67948f120e731f64b9ef437a041c65 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:10:08 -0400 Subject: [PATCH 17/25] Add test with exporting interfaces for the inheritance --- .../associations/modulesInheritance.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/incremental-update/associations/modulesInheritance.test.ts b/test/incremental-update/associations/modulesInheritance.test.ts index b0ffedd2..1865e065 100644 --- a/test/incremental-update/associations/modulesInheritance.test.ts +++ b/test/incremental-update/associations/modulesInheritance.test.ts @@ -219,4 +219,53 @@ describe('Change the inheritance between several files', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); + + it('should handle a chain of the interfaces with inheritance when super class changed 2 times', () => { + // arrange + const codeA = `export interface A { }`; + const codeAChanged = ` + import { SomeUndefined } from './unexistingModule.ts'; + export interface A extends SomeUndefined { }`; + const codeAChangedTwice = ` + import { OtherUndefined } from './unexistingModule.ts'; + export interface A extends OtherUndefined { }`; + const codeAFileName = 'codeA.ts'; + + const codeB = `import { A } from './codeA'; + export interface B extends A { }`; + const codeBFileName = 'codeB.ts'; + + const codeC = `import { B } from './codeB'; + export interface C extends B { }`; + const codeCFileName = 'codeC.ts'; + + + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(codeAFileName, codeA) + .addSourceFile(codeBFileName, codeB) + .addSourceFile(codeCFileName, codeC); + const { importer, famixRep } = testProjectBuilder.build(); + + const sourceFile = testProjectBuilder + .changeSourceFile(codeAFileName, codeAChanged); + + // act + let fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + testProjectBuilder.changeSourceFile(codeAFileName, codeAChangedTwice); + + fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + // assert + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [codeAFileName, codeAChangedTwice], + [codeBFileName, codeB], + [codeCFileName, codeC] + ]); + + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); }); \ No newline at end of file From ad758b6a5904510d430d446cf6ac75a286a6c31f Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:20:51 -0400 Subject: [PATCH 18/25] Remove the old ImportClause test --- test/importClause.test.ts | 181 -------------------------------------- 1 file changed, 181 deletions(-) delete mode 100644 test/importClause.test.ts diff --git a/test/importClause.test.ts b/test/importClause.test.ts deleted file mode 100644 index 4c378208..00000000 --- a/test/importClause.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { Importer } from "../src/analyze"; -import { Class, ImportClause, Module, NamedEntity, StructuralEntity } from "../src/lib/famix/model/famix"; -import { project } from './testUtils'; - -const importer = new Importer(); -//logger.settings.minLevel = 0; // all your messages are belong to us - -project.createSourceFile("/test_src/oneClassExporter.ts", - `export class ExportedClass {}`); - -project.createSourceFile("/test_src/oneClassImporter.ts", - `import { ExportedClass } from "./oneClassExporter";`); - -project.createSourceFile("/test_src/complexExportModule.ts", - `class ClassZ {} -class ClassY {} -export class ClassX {} - -export { ClassZ, ClassY }; -export { Importer } from '../src/analyze'; - -export default class ClassW {} - -export namespace Nsp {} -`); - -project.createSourceFile("/test_src/defaultImporterModule.ts", - `import * as test from "./complexExportModule.ts";`); - -project.createSourceFile("/test_src/multipleClassImporterModule.ts", - `import { ClassZ } from "./complexExportModule.ts";`); - -project.createSourceFile("/test_src/reExporterModule.ts", - `export * from "./complexExportModule.ts";`); - -project.createSourceFile("/test_src/reImporterModule.ts", - `import { ClassX } from "./reExporterModule.ts";`); - -project.createSourceFile("/test_src/renameDefaultExportImporter.ts", - `import myRenamedDefaultClassW from "./complexExportModule.ts";`); - -project.createSourceFile("lazyRequireModuleCommonJS.ts", - `import foo = require('foo'); - - export function loadFoo() { - // This is lazy loading "foo" and using the original module *only* as a type annotation - var _foo: typeof foo = require('foo'); - // Now use "_foo" as a variable instead of "foo". - }`); // see https://basarat.gitbook.io/typescript/project/modules/external-modules#use-case-lazy-loading - -const fmxRep = importer.famixRepFromProject(project); -const NUMBER_OF_MODULES = 10, - NUMBER_OF_IMPORT_CLAUSES = 6; - -const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; -const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; -const entityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; - -describe('Tests for import clauses', () => { - it(`should have ${NUMBER_OF_MODULES} modules`, () => { - expect(moduleList?.length).toBe(NUMBER_OF_MODULES); - const exporterModule = moduleList.find(e => e.name === 'oneClassExporter.ts'); - expect(exporterModule).toBeTruthy(); - const importerModule = moduleList.find(e => e.name === 'oneClassImporter.ts'); - expect(importerModule).toBeTruthy(); - const complexModule = moduleList.find(e => e.name === 'complexExportModule.ts'); - expect(complexModule).toBeTruthy(); - // add the expects for the modules defaultImporterModule, multipleClassImporterModule, reExporterModule, reImporterModule, renameDefaultExportImporter - const defaultImporterModule = moduleList.find(e => e.name === 'defaultImporterModule.ts'); - expect(defaultImporterModule).toBeTruthy(); - const multipleClassImporterModule = moduleList.find(e => e.name === 'multipleClassImporterModule.ts'); - expect(multipleClassImporterModule).toBeTruthy(); - const reExporterModule = moduleList.find(e => e.name === 'reExporterModule.ts'); - expect(reExporterModule).toBeTruthy(); - const reImporterModule = moduleList.find(e => e.name === 'reImporterModule.ts'); - expect(reImporterModule).toBeTruthy(); - const renameDefaultExportImporter = moduleList.find(e => e.name === 'renameDefaultExportImporter.ts'); - expect(renameDefaultExportImporter).toBeTruthy(); - // const ambientModule = moduleList.find(e => e.name === 'renameDefaultExportImporter.ts'); - // expect(renameDefaultExportImporter).toBeTruthy(); - }); - - it(`should have ${NUMBER_OF_IMPORT_CLAUSES} import clauses`, () => { - expect(importClauses).toBeTruthy(); - expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); - }); - - it("should import myRenamedDefaultClassW that is a renamed ClassW from module complexExportModule.ts", () => { - // find the import clause for ClassW - const importClause = importClauses.find(e => e.importedEntity?.name === 'myRenamedDefaultClassW'); - expect(importClause).toBeTruthy(); - // expect the imported entity to be ClassW - const importedEntity = importClause?.importedEntity as Class; - expect(importedEntity?.name).toBe("myRenamedDefaultClassW"); - // importing entity is renameDefaultExportImporter.ts - const importingEntity = importClause?.importingEntity; - expect(importingEntity).toBeTruthy(); - expect(importingEntity?.name).toBe("renameDefaultExportImporter.ts"); - }); - - it("should import ClassX from module that exports from an import", () => { - // find the import clause for ClassX - const importClause = importClauses.find(e => e.importedEntity?.name === 'ClassX'); - expect(importClause).toBeTruthy(); - // importing entity is reImporterModule.ts - expect(importClause?.importingEntity).toBeTruthy(); - expect(importClause?.importingEntity?.name).toBe("reImporterModule.ts"); - }); - - it("should import ClassZ from module complexExporterModule.ts", () => { - // find the import clause for ClassZ - const importClause = importClauses.find(e => e.importedEntity?.name === 'ClassZ'); - expect(importClause).toBeTruthy(); - // importing entity is multipleClassImporterModule.ts - expect(importClause?.importingEntity).toBeTruthy(); - expect(importClause?.importingEntity?.name).toBe("multipleClassImporterModule.ts"); - }); - - it("should have a default import clause for test", () => { - expect(importClauses).toBeTruthy(); - // find the import clause for ClassW - const importClause = importClauses.find(e => e.importedEntity?.name === 'test'); - expect(importClause).toBeTruthy(); - // importing entity is complexExportModule.ts - expect(importClause?.importingEntity).toBeTruthy(); - expect(importClause?.importingEntity?.name).toBe("defaultImporterModule.ts"); - }); - - it("should contain an import clause for ExportedClass", () => { - expect(importClauses).toBeTruthy(); - const importClause = importClauses.find(e => e.importedEntity?.name === 'test'); - expect(importClause).toBeTruthy(); - // importing entity is oneClassImporter.ts - expect(importClause?.importingEntity).toBeTruthy(); - expect(importClause?.importingEntity?.name).toBe("defaultImporterModule.ts"); - }); - - it("should contain one outgoingImports element for module oneClassImporter.ts", () => { - const importerModule = moduleList.find(e => e.name === 'oneClassImporter.ts'); - expect(importerModule).toBeTruthy(); - expect(importerModule?.outgoingImports).toBeTruthy(); - expect(importerModule?.outgoingImports?.size).toBe(1); - expect(importerModule?.outgoingImports?.values().next().value.importedEntity?.name).toBe("ExportedClass"); - }); - - it("should contain one imports element for module oneClassExporter.ts", () => { - const exportedEntity = entityList.find(e => e.name === 'ExportedClass'); - expect(exportedEntity).toBeTruthy(); - expect(exportedEntity?.incomingImports).toBeTruthy(); - expect(exportedEntity?.incomingImports?.size).toBe(1); - expect(exportedEntity?.incomingImports?.values().next().value.importingEntity?.name).toBe("oneClassImporter.ts"); - }); - - it("should have import clauses with source code anchors", () => { - // expect the import clause from renameDefaultExportImporter.ts to have a source anchor for "" - const importClause = importClauses.find(e => e.importedEntity?.name === 'myRenamedDefaultClassW'); - expect(importClause).toBeTruthy(); - // const fileAnchor = importClause?.getSourceAnchor() as IndexedFileAnchor; - // expect(fileAnchor).toBeTruthy(); - // const fileName = fileAnchor?.fileName.split("/").pop(); - // expect(fileName).toBe("renameDefaultExportImporter.ts"); - // expect the text from the file anchor to be "" - // expect(getTextFromAnchor(fileAnchor, project)).toBe(`import myRenamedDefaultClassW from "./complexExportModule.ts";`); - }); - - it("should have an import clause for require('foo')", () => { - // find the import clause for foo - const importClause = importClauses.find(e => e.importedEntity?.name === 'foo'); - expect(importClause).toBeTruthy(); - // importing entity is lazyRequireModuleCommonJS.ts - expect(importClause?.importingEntity).toBeTruthy(); - expect(importClause?.importingEntity?.name).toBe("lazyRequireModuleCommonJS.ts"); - // const fileAnchor = importClause?.getSourceAnchor() as IndexedFileAnchor; - // expect(getTextFromAnchor(fileAnchor, project)).toBe(`import foo = require('foo');`); - // expect the type of the importedEntity to be "StructuralEntity" - expect((importClause?.importedEntity.constructor.name)).toBe("StructuralEntity"); - // expect the type of foo to be any - expect((importClause?.importedEntity as StructuralEntity).declaredType?.name).toBe("any"); - }); - -}); From 00b414a4642e8c751d1d2c9c57a3508105d91dc3 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:27:01 -0400 Subject: [PATCH 19/25] Add todo to the module tests --- test/module.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/module.test.ts b/test/module.test.ts index 4df1ddfa..ffe819ad 100644 --- a/test/module.test.ts +++ b/test/module.test.ts @@ -2,6 +2,8 @@ import { Importer, logger } from '../src/analyze'; import { Module } from '../src/lib/famix/model/famix/module'; import { project } from './testUtils'; +// TODO: implement the default import and check if this test is still correct and up to date + const importer = new Importer(); project.createSourceFile("/test_src/moduleBecauseExports.ts", ` From 1f7cd9872d85dd52b71d735fcc4df8cdba84aaba Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:20:08 -0400 Subject: [PATCH 20/25] Fix resolving file path for the transient dependency search --- src/analyze.ts | 4 +++- src/helpers/famixIndexFileAnchorHelper.ts | 4 ---- src/helpers/incrementalUpdateHelper.ts | 13 +++++++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index b3340169..eaba8f0a 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -148,7 +148,9 @@ export class Importer { removeDependentAssociations(this.entityDictionary.famixRep, associationsToRemove); - const sourceFilesToEnsure = getSourceFilesToUpdate(associationsToRemove, sourceFileChangeMap, allSourceFiles); + const sourceFilesToEnsure = getSourceFilesToUpdate( + associationsToRemove, sourceFileChangeMap, allSourceFiles, this.entityDictionary.getAbsolutePath() + ); this.processFunctions.processFiles(sourceFilesToEnsure); const sourceFilesToDelete = sourceFileChangeMap.get(SourceFileChangeType.Delete) || []; diff --git a/src/helpers/famixIndexFileAnchorHelper.ts b/src/helpers/famixIndexFileAnchorHelper.ts index b21d573f..2325704e 100644 --- a/src/helpers/famixIndexFileAnchorHelper.ts +++ b/src/helpers/famixIndexFileAnchorHelper.ts @@ -22,7 +22,3 @@ export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePa } return pathInProject; }; - -export const getAbsoluteFileNameFromFamixIndexFileAnchor = (famixIndexFileAnchor: string) => { - return `/${famixIndexFileAnchor}`; -}; \ No newline at end of file diff --git a/src/helpers/incrementalUpdateHelper.ts b/src/helpers/incrementalUpdateHelper.ts index 57f47b69..a3bb0210 100644 --- a/src/helpers/incrementalUpdateHelper.ts +++ b/src/helpers/incrementalUpdateHelper.ts @@ -4,15 +4,17 @@ import { ImportClause, IndexedFileAnchor, Inheritance, Interface, NamedEntity } import { EntityWithSourceAnchor } from '../lib/famix/model/famix/sourced_entity'; import { SourceFileChangeType } from '../analyze'; import { SourceFile } from 'ts-morph'; -import { getAbsoluteFileNameFromFamixIndexFileAnchor, getFamixIndexFileAnchorFileName } from './famixIndexFileAnchorHelper'; +import { getFamixIndexFileAnchorFileName } from './famixIndexFileAnchorHelper'; import { FamixRepository } from '../lib/famix/famix_repository'; import { EntityDictionary } from 'src/famix_functions/EntityDictionary'; import { getTransientDependentAssociations } from './transientDependencyResolverHelper'; + export const getSourceFilesToUpdate = ( dependentAssociations: EntityWithSourceAnchor[], sourceFileChangeMap: Map, - allSourceFiles: SourceFile[] + allSourceFiles: SourceFile[], + projectBaseUrl: string ) => { const sourceFilesToEnsureEntities = [ ...(sourceFileChangeMap.get(SourceFileChangeType.Create) || []), @@ -21,13 +23,16 @@ export const getSourceFilesToUpdate = ( const dependentFileNames = getDependentSourceFileNames(dependentAssociations); const dependentFileNamesToAdd = Array.from(dependentFileNames) - .map(fileName => getAbsoluteFileNameFromFamixIndexFileAnchor(fileName)) + .map(fileName => getFamixIndexFileAnchorFileName(fileName, projectBaseUrl)) .filter( fileName => !Array.from(sourceFileChangeMap.values()) .flat().some(sourceFile => sourceFile.getFilePath() === fileName)); const dependentFiles = allSourceFiles.filter( - sourceFile => dependentFileNamesToAdd.includes(sourceFile.getFilePath()) + sourceFile => { + const filePath = getFamixIndexFileAnchorFileName(sourceFile.getFilePath(), projectBaseUrl); + return dependentFileNamesToAdd.includes(filePath); + } ); return sourceFilesToEnsureEntities.concat(dependentFiles); From de6192575aff809054969e8e9d40fbca409f43b1 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:17:38 -0400 Subject: [PATCH 21/25] Fix getFamixEntityByFullyQualifiedName to work with all the entities that have fullyQualifiedName field --- src/lib/famix/famix_repository.ts | 3 ++- .../associations/importClauseReExport.test.ts | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index 5984b35e..beab7c2c 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -4,6 +4,7 @@ import * as Famix from "./model/famix"; import { TSMorphObjectType } from "../../famix_functions/EntityDictionary"; import { logger } from "../../analyze"; import { EntityWithSourceAnchor } from "./model/famix/sourced_entity"; +import { FullyQualifiedNameEntity } from "./model/interfaces/fully_qualified_name_entity"; /** * This class is used to store all Famix elements @@ -41,7 +42,7 @@ export class FamixRepository { * @returns The Famix entity corresponding to the fully qualified name or undefined if it doesn't exist */ public getFamixEntityByFullyQualifiedName(fullyQualifiedName: string): T | undefined { - const allEntities = Array.from(this.elements.values()).filter(e => e instanceof NamedEntity) as Array; + const allEntities = Array.from(this.elements.values()).filter(e => (e as NamedEntity).fullyQualifiedName) as Array; const entity = allEntities.find(e => // {console.log(`namedEntity: ${e.fullyQualifiedName}`); // return diff --git a/test/incremental-update/associations/importClauseReExport.test.ts b/test/incremental-update/associations/importClauseReExport.test.ts index 1650a414..187a2f75 100644 --- a/test/incremental-update/associations/importClauseReExport.test.ts +++ b/test/incremental-update/associations/importClauseReExport.test.ts @@ -480,6 +480,31 @@ describe('Namespace import re-export with inheritance changes', () => { expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); }); + + it('should handle removing original export', () => { + // arrange + const testProjectBuilder = new IncrementalUpdateProjectBuilder(); + testProjectBuilder + .addSourceFile(exportSourceFileName, initialExportCode) + .addSourceFile(reexportSourceFileName, reexportCode) + .addSourceFile(importSourceFileName, importCode); + + const { importer, famixRep } = testProjectBuilder.build(); + + // act - remove re-export + const sourceFile = testProjectBuilder.changeSourceFile(exportSourceFileName, ''); + const fileChangesMap = getUpdateFileChangesMap(sourceFile); + importer.updateFamixModelIncrementally(fileChangesMap); + + const expectedFamixRepo = createExpectedFamixModelForSeveralFiles([ + [exportSourceFileName, ''], + [reexportSourceFileName, reexportCode], + [importSourceFileName, importCode] + ]); + + // assert + expectRepositoriesToHaveSameStructure(famixRep, expectedFamixRepo); + }); }); describe('5-file namespace import re-export chain test', () => { From 5a92ba72d2f27ef6527b21c84378600eeb6cf6c0 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:30:39 -0400 Subject: [PATCH 22/25] Fix getModuleSpecifierFromDeclaration to work with import without ts. We still need to verify how does it work with node_modules and import aliases --- src/famix_functions/ImportClauseCreator.ts | 10 +++++++++- src/helpers/incrementalUpdateHelper.ts | 2 +- src/helpers/transientDependencyResolverHelper.ts | 1 + test/incremental-update/incrementalUpdateExpect.ts | 7 ++++--- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/famix_functions/ImportClauseCreator.ts b/src/famix_functions/ImportClauseCreator.ts index 328992fd..a6d5c509 100644 --- a/src/famix_functions/ImportClauseCreator.ts +++ b/src/famix_functions/ImportClauseCreator.ts @@ -110,8 +110,16 @@ export class ImportClauseCreator { } private getModuleSpecifierFromDeclaration(importOrExportDeclaration: ImportDeclaration | ExportDeclaration): string { + let moduleSpecifierFileName = importOrExportDeclaration.getModuleSpecifierValue(); + // TODO: test this path finding with node modules, declaration files, etc. + // It is important that this name can be used later for finding the file name which is used for the source anchor + if (moduleSpecifierFileName && !moduleSpecifierFileName.endsWith('.ts')) { + moduleSpecifierFileName = moduleSpecifierFileName + '.ts'; + } + //------------------------------- + return getFamixIndexFileAnchorFileName( - importOrExportDeclaration.getModuleSpecifierValue() ?? '', + moduleSpecifierFileName ?? '', this.entityDictionary.getAbsolutePath() ); } diff --git a/src/helpers/incrementalUpdateHelper.ts b/src/helpers/incrementalUpdateHelper.ts index a3bb0210..f9846ee7 100644 --- a/src/helpers/incrementalUpdateHelper.ts +++ b/src/helpers/incrementalUpdateHelper.ts @@ -9,7 +9,7 @@ import { FamixRepository } from '../lib/famix/famix_repository'; import { EntityDictionary } from 'src/famix_functions/EntityDictionary'; import { getTransientDependentAssociations } from './transientDependencyResolverHelper'; - +// TODO: add tests for these methods export const getSourceFilesToUpdate = ( dependentAssociations: EntityWithSourceAnchor[], sourceFileChangeMap: Map, diff --git a/src/helpers/transientDependencyResolverHelper.ts b/src/helpers/transientDependencyResolverHelper.ts index c07dca9c..3a753756 100644 --- a/src/helpers/transientDependencyResolverHelper.ts +++ b/src/helpers/transientDependencyResolverHelper.ts @@ -2,6 +2,7 @@ import { EntityWithSourceAnchor } from "../lib/famix/model/famix/sourced_entity" import { EntityDictionary } from "../famix_functions/EntityDictionary"; import { Class, ImportClause, IndexedFileAnchor, Interface } from "../lib/famix/model/famix"; +// TODO: add tests for these methods export const getTransientDependentAssociations = ( entityDictionary: EntityDictionary, changedFilesNames: string [] diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 5c29626b..14a611f7 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -7,7 +7,8 @@ const namedEntityCompareFunction = (actual: FamixBaseElement, expected: FamixBas const expectedAsNamedEntity = expected as NamedEntity; return actualAsNamedEntity.fullyQualifiedName === expectedAsNamedEntity.fullyQualifiedName && - actualAsNamedEntity.incomingImports.size === expectedAsNamedEntity.incomingImports.size; + actualAsNamedEntity.incomingImports.size === expectedAsNamedEntity.incomingImports.size && + actualAsNamedEntity.isStub === expectedAsNamedEntity.isStub; }; const classCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { @@ -79,7 +80,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Function"); expectElementsToBeEqualSize(actual, expected, "ImportClause"); expectElementsToBeSame(actual, expected, "ImportClause", importClauseCompareFunction); - // expectElementsToBeEqualSize(actual, expected, "IndexedFileAnchor"); + expectElementsToBeEqualSize(actual, expected, "IndexedFileAnchor"); expectElementsToBeEqualSize(actual, expected, "Inheritance"); expectElementsToBeSame(actual, expected, "Inheritance", inheritanceCompareFunction); expectElementsToBeEqualSize(actual, expected, "Interface"); @@ -110,7 +111,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Type"); expectElementsToBeEqualSize(actual, expected, "Variable"); - // expect(actual._getAllEntities().size).toEqual(expected._getAllEntities().size); + expect(actual._getAllEntities().size).toEqual(expected._getAllEntities().size); }; const expectElementsToBeEqualSize = (actual: FamixRepository, expected: FamixRepository, type: string) => { From 611606a8e7b887d01ef8821d545af6c48df1d529 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:50:42 -0400 Subject: [PATCH 23/25] Add a comment --- src/lib/famix/famix_repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/famix/famix_repository.ts b/src/lib/famix/famix_repository.ts index beab7c2c..b801eb69 100644 --- a/src/lib/famix/famix_repository.ts +++ b/src/lib/famix/famix_repository.ts @@ -112,6 +112,7 @@ export class FamixRepository { } } + // NOTE: consider storing all the associations (ImportClause, Inheritance, ...) if we need a better performance public getImportClauses(): Famix.ImportClause[] { return Array.from(this.elements.values()).filter(e => e instanceof Famix.ImportClause) as Famix.ImportClause[]; } From 859ef1514e41c75d7cd741b83138976c7475c910 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:41:21 -0400 Subject: [PATCH 24/25] Add more properties to Module. Rename methods --- src/analyze_functions/process_functions.ts | 4 ++-- src/famix_functions/EntityDictionary.ts | 11 ++++++----- src/lib/famix/model/famix/module.ts | 4 ++-- .../incrementalUpdateExpect.ts | 18 +++++++++++++----- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index abc999f9..60e66e51 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -73,7 +73,7 @@ export class TypeScriptToFamixProcessor { * @param f A source file */ private processFile(f: SourceFile): void { - const fmxFile = this.entityDictionary.createOrGetFamixFile(f); + const fmxFile = this.entityDictionary.ensureFamixFile(f); logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`); this.processComments(f, fmxFile); @@ -92,7 +92,7 @@ export class TypeScriptToFamixProcessor { * @returns A Famix.Module representing the module */ private processModule(m: ModuleDeclaration): Famix.Module { - const fmxModule = this.entityDictionary.createOrGetFamixModule(m); + const fmxModule = this.entityDictionary.ensureFamixModule(m); logger.debug(`module: ${m.getName()}, (${m.getType().getText()}), ${fmxModule.fullyQualifiedName}`); diff --git a/src/famix_functions/EntityDictionary.ts b/src/famix_functions/EntityDictionary.ts index 01f801cb..88bd0afe 100644 --- a/src/famix_functions/EntityDictionary.ts +++ b/src/famix_functions/EntityDictionary.ts @@ -215,16 +215,17 @@ export class EntityDictionary { * @param isModule A boolean indicating if the source file is a module * @returns The Famix model of the source file */ - public createOrGetFamixFile(f: SourceFile): Famix.ScriptEntity | Famix.Module { + public ensureFamixFile(f: SourceFile): Famix.ScriptEntity | Famix.Module { const mapToFamixElement = (f: SourceFile) => { - let fmxFile: Famix.ScriptEntity; // | Famix.Module; + let fmxFile: Famix.ScriptEntity | Famix.Module; const fileName = f.getBaseName(); const isModule = isSourceFileAModule(f); - // TODO: do we need to create a module here instead of ScriptEntity? - // If so - we need to add other properties to the module if (isModule) { fmxFile = new Famix.Module(); + (fmxFile as Famix.Module).isAmbient = false; + (fmxFile as Famix.Module).isNamespace = false; + (fmxFile as Famix.Module).isModule = true; } else { fmxFile = new Famix.ScriptEntity(); @@ -245,7 +246,7 @@ export class EntityDictionary { * @param moduleDeclaration A module * @returns The Famix model of the module */ - public createOrGetFamixModule(moduleDeclaration: ModuleDeclaration): Famix.Module { + public ensureFamixModule(moduleDeclaration: ModuleDeclaration): Famix.Module { const mapToFamixElement = (moduleDeclaration: ModuleDeclaration) => { const fmxModule = new Famix.Module(); const moduleName = moduleDeclaration.getName(); diff --git a/src/lib/famix/model/famix/module.ts b/src/lib/famix/model/famix/module.ts index f4ae41a7..93105897 100644 --- a/src/lib/famix/model/famix/module.ts +++ b/src/lib/famix/model/famix/module.ts @@ -34,7 +34,7 @@ export class Module extends ScriptEntity { private _isModule: boolean = true; - private _parentScope!: ScopingEntity; + private _parentScope?: ScopingEntity; // incomingImports are in NamedEntity private _outgoingImports: Set = new Set(); @@ -62,7 +62,7 @@ export class Module extends ScriptEntity { exporter.addProperty("outgoingImports", this.outgoingImports); } - get parentScope() { + get parentScope(): ScopingEntity | undefined { return this._parentScope; } diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 14a611f7..3db56f4f 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -1,4 +1,4 @@ -import { FamixBaseElement, ImportClause, Inheritance, Module, NamedEntity } from "../../src"; +import { FamixBaseElement, ImportClause, Inheritance, Module, NamedEntity, ScriptEntity } from "../../src"; import { FamixRepository } from "../../src"; import { Class, PrimitiveType } from "../../src"; @@ -51,13 +51,20 @@ const moduleCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElem const actualAsModule = actual as Module; const expectedAsModule = expected as Module; - return namedEntityCompareFunction(actualAsModule, expectedAsModule) && + return scriptEntityCompareFunction(actualAsModule, expectedAsModule) && actualAsModule.isAmbient === expectedAsModule.isAmbient && actualAsModule.isNamespace === expectedAsModule.isNamespace && actualAsModule.isModule === expectedAsModule.isModule && - // TODO: do we use the module correctly (inside the createFamixFile method?) - actualAsModule.parentScope?.fullyQualifiedName === expectedAsModule.parentScope?.fullyQualifiedName && - actualAsModule.incomingImports.size === expectedAsModule.incomingImports.size; + actualAsModule.parentScope?.fullyQualifiedName === expectedAsModule.parentScope?.fullyQualifiedName; +}; + +const scriptEntityCompareFunction = (actual: FamixBaseElement, expected: FamixBaseElement) => { + const actualAsScriptEntity = actual as ScriptEntity; + const expectedAsScriptEntity = expected as ScriptEntity; + + return namedEntityCompareFunction(actualAsScriptEntity, expectedAsScriptEntity) && + actualAsScriptEntity.numberOfLinesOfText === expectedAsScriptEntity.numberOfLinesOfText && + actualAsScriptEntity.numberOfCharacters === expectedAsScriptEntity.numberOfCharacters; }; export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, expected: FamixRepository) => { @@ -104,6 +111,7 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Reference"); expectElementsToBeEqualSize(actual, expected, "ScopingEntity"); expectElementsToBeEqualSize(actual, expected, "ScriptEntity"); + expectElementsToBeSame(actual, expected, "ScriptEntity", scriptEntityCompareFunction); expectElementsToBeEqualSize(actual, expected, "SourceAnchor"); expectElementsToBeEqualSize(actual, expected, "SourceLanguage"); expectElementsToBeEqualSize(actual, expected, "SourcedEntity"); From 8a2e4ab3f7e7de0fd13d54ac2780c716bb3ae798 Mon Sep 17 00:00:00 2001 From: Lidiia Makarchuk <87641510+norilanda@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:57:01 -0400 Subject: [PATCH 25/25] Add todo for import clause: currently we have a bug where namedEntities stubs can be duplicated. Add default import tests --- src/analyze_functions/process_functions.ts | 4 +- src/famix_functions/ImportClauseCreator.ts | 21 +- .../helpersTsMorphElementsProcessing.ts | 4 +- test/importClauseDefaultExports.test.ts | 185 ++++++++++++++++-- 4 files changed, 197 insertions(+), 17 deletions(-) diff --git a/src/analyze_functions/process_functions.ts b/src/analyze_functions/process_functions.ts index 60e66e51..2ceb65f4 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -836,6 +836,7 @@ export class TypeScriptToFamixProcessor { const defaultImport = impDecl.getDefaultImport(); if (defaultImport !== undefined) { this.importClauseCreator.ensureFamixImportClauseForDefaultImport( + impDecl, defaultImport, ); } @@ -843,8 +844,7 @@ export class TypeScriptToFamixProcessor { const namespaceImport = impDecl.getNamespaceImport(); if (namespaceImport !== undefined) { this.importClauseCreator.ensureFamixImportClauseForNamespaceImport( - namespaceImport, - module + namespaceImport, module, module ); } }); diff --git a/src/famix_functions/ImportClauseCreator.ts b/src/famix_functions/ImportClauseCreator.ts index a6d5c509..a148a955 100644 --- a/src/famix_functions/ImportClauseCreator.ts +++ b/src/famix_functions/ImportClauseCreator.ts @@ -17,7 +17,7 @@ export class ImportClauseCreator { public ensureFamixImportClauseForNamedImport( importDeclaration: ImportDeclaration | ExportDeclaration, - namedImport: ImportSpecifier | ExportSpecifier, + namedImport: ImportSpecifier | ExportSpecifier | Identifier, importingSourceFile: SourceFile ) { const namedEntityDeclaration = getDeclarationFromImportOrExport(namedImport); @@ -55,12 +55,20 @@ export class ImportClauseCreator { const reexportedExports = moduleSpecifierSymbol.getExports(); this.handleNamespaceImportOrExport(reexportedExports, importingEntity, moduleSpecifier, exportDeclaration); } else { + // TODO: add a stub to the repo only when it is checked that the import clause does not exists yet + // to avoid stub duplication const importedEntity = this.ensureImportedEntityStub(exportDeclaration); this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, exportDeclaration); } } - public ensureFamixImportClauseForDefaultImport(defaultImport: Identifier) { + public ensureFamixImportClauseForDefaultImport( + importDeclaration: ImportDeclaration, defaultImport: Identifier, module: SourceFile + ) { + const namedEntityDeclaration = getDeclarationFromImportOrExport(defaultImport); + const moduleSpecifier = this.getModuleSpecifierFromDeclaration(importDeclaration); + + // TODO: finish implementation throw new Error("Not implemented"); } @@ -77,6 +85,9 @@ export class ImportClauseCreator { } if (!importedEntity) { // TODO: check how do we create the FQN for the import specifier + + // TODO: add a stub to the repo only when it is checked that the import clause does not exists yet + // to avoid stub duplication importedEntity = this.ensureImportedEntityStub(importedEntityDeclaration); } return importedEntity; @@ -159,6 +170,8 @@ export class ImportClauseCreator { // It no exports found - create a stub if (exportsOfModuleSet.size === 0) { + // TODO: add a stub to the repo only when it is checked that the import clause does not exists yet + // to avoid stub duplication importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); return; @@ -172,6 +185,8 @@ export class ImportClauseCreator { exportsOfModuleSet.add(aliasedSmb); } } else { // else - it means the re-export chain is broken + // TODO: add a stub to the repo only when it is checked that the import clause does not exists yet + // to avoid stub duplication importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); } @@ -187,6 +202,8 @@ export class ImportClauseCreator { } }); } else { // else - it means the re-export chain is broken + // TODO: add a stub to the repo only when it is checked that the import clause does not exists yet + // to avoid stub duplication importedEntity = this.ensureImportedEntityStub(importOrExportDeclaration); this.ensureFamixImportClause(importedEntity, importingEntity, moduleSpecifier, importOrExportDeclaration); } diff --git a/src/famix_functions/helpersTsMorphElementsProcessing.ts b/src/famix_functions/helpersTsMorphElementsProcessing.ts index 9be7c95b..575d5d92 100644 --- a/src/famix_functions/helpersTsMorphElementsProcessing.ts +++ b/src/famix_functions/helpersTsMorphElementsProcessing.ts @@ -1,4 +1,4 @@ -import { ArrowFunction, ClassDeclaration, ExportSpecifier, ExpressionWithTypeArguments, ImportSpecifier, +import { ArrowFunction, ClassDeclaration, ExportSpecifier, ExpressionWithTypeArguments, Identifier, ImportSpecifier, InterfaceDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind, ts } from "ts-morph"; import { Symbol as TSMorphSymbol } from "ts-morph"; @@ -104,7 +104,7 @@ function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): Inte return undefined; } -export const getDeclarationFromImportOrExport = (importOrExport: ImportSpecifier | ExportSpecifier): Node | undefined => { +export const getDeclarationFromImportOrExport = (importOrExport: ImportSpecifier | ExportSpecifier | Identifier): Node | undefined => { const symbol = importOrExport.getSymbol(); const aliasedSymbol = symbol?.getAliasedSymbol(); diff --git a/test/importClauseDefaultExports.test.ts b/test/importClauseDefaultExports.test.ts index 1ae15d79..78d2b410 100644 --- a/test/importClauseDefaultExports.test.ts +++ b/test/importClauseDefaultExports.test.ts @@ -1,4 +1,4 @@ -import { Class, ImportClause, Module, StructuralEntity } from '../src'; +import { Class, ImportClause, Module, NamedEntity } from '../src'; import { Importer } from '../src/analyze'; import { createProject } from './testUtils'; @@ -24,7 +24,39 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + it("should work with default exports when declared and exported separately", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + export default Animal; + `); + + project.createSourceFile("/importingFile.ts", + `import Animal from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -55,7 +87,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -86,7 +118,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -96,6 +128,44 @@ describe('Import Clause Default Exports', () => { expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); }); + it("should work with multiple default exports in a single file", () => { + const importer = new Importer(); + const project = createProject(); + + // how to implement this??? + project.createSourceFile("/moduleA.ts", + ` + class ClassA {} + class ClassB {} + export default { ClassA, ClassB }; + `); + + project.createSourceFile("/importingFile.ts", + `import classes from './moduleA'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 2; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + + const classAImport = importClauses.find(ic => ic.importedEntity.name === 'ClassA'); + + expect(classAImport).toBeTruthy(); + }); + it("should work with multiple default exports", () => { const importer = new Importer(); const project = createProject(); @@ -122,7 +192,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -160,7 +230,35 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with default exports for expressions", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default 42 + 3; + `); + + project.createSourceFile("/importingFile.ts", + `import someNumber from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); @@ -189,7 +287,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -220,7 +318,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -230,6 +328,71 @@ describe('Import Clause Default Exports', () => { expect(importClauses[0].importedEntity.fullyQualifiedName).not.toBe(classesList[0].fullyQualifiedName); }); + it("should work with default exports and namespace import", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export default class Animal {} + `); + + project.createSourceFile("/importingFile.ts", + `import * as Pet from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 0; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + expect(importClauses[0].importedEntity.fullyQualifiedName).toBe(classesList[0].fullyQualifiedName); + }); + + + it("should work with default exports and namespace import", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `class Animal {} + const id = 42; + export default { Animal, id } + `); + + project.createSourceFile("/importingFile.ts", + `import * as Pet from './exportingFile'; + `); + + const fmxRep = importer.famixRepFromProject(project); + + const NUMBER_OF_MODULES = 2; + const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_STUB_ENTITIES = 1; + const NUMBER_OF_CLASSES = 1; + + const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; + const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; + const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; + + expect(moduleList.length).toBe(NUMBER_OF_MODULES); + expect(importClauses.length).toBe(NUMBER_OF_IMPORT_CLAUSES); + expect(stubEntityList.length).toBe(NUMBER_OF_STUB_ENTITIES); + expect(classesList.length).toBe(NUMBER_OF_CLASSES); + }); + + // ? it("should work with default exports and re-export", () => { const importer = new Importer(); const project = createProject(); @@ -255,7 +418,7 @@ describe('Import Clause Default Exports', () => { const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES); @@ -284,13 +447,13 @@ describe('Import Clause Default Exports', () => { const fmxRep = importer.famixRepFromProject(project); const NUMBER_OF_MODULES = 3; - const NUMBER_OF_IMPORT_CLAUSES = 1; + const NUMBER_OF_IMPORT_CLAUSES = 2; const NUMBER_OF_STUB_ENTITIES = 0; const NUMBER_OF_CLASSES = 1; const importClauses = Array.from(fmxRep._getAllEntitiesWithType("ImportClause")) as Array; const moduleList = Array.from(fmxRep._getAllEntitiesWithType('Module')) as Array; - const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('StructuralEntity')) as Array; + const stubEntityList = Array.from(fmxRep._getAllEntitiesWithType('NamedEntity')) as Array; const classesList = Array.from(fmxRep._getAllEntitiesWithType('Class')) as Array; expect(moduleList.length).toBe(NUMBER_OF_MODULES);