diff --git a/src/analyze.ts b/src/analyze.ts index c38855ce..eaba8f0a 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, getTransientDependentEntities } 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,35 @@ 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); + const transientDependentAssociations = getTransientDependentEntities(this.entityDictionary, sourceFileChangeMap); + + const associationsToRemove = [...dependentAssociations, ...transientDependentAssociations]; + + removeDependentAssociations(this.entityDictionary.famixRep, associationsToRemove); + + const sourceFilesToEnsure = getSourceFilesToUpdate( + associationsToRemove, sourceFileChangeMap, allSourceFiles, this.entityDictionary.getAbsolutePath() + ); + + 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..2ceb65f4 100644 --- a/src/analyze_functions/process_functions.ts +++ b/src/analyze_functions/process_functions.ts @@ -1,12 +1,13 @@ -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"; +import { ImportClauseCreator } from "../famix_functions/ImportClauseCreator"; export type AccessibleTSMorphElement = ParameterDeclaration | VariableDeclaration | PropertyDeclaration | EnumMember; export type FamixID = number; @@ -15,56 +16,23 @@ 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; + 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 - 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 constructor(entityDictionary: EntityDictionary) { this.entityDictionary = entityDictionary; + this.importClauseCreator = new ImportClauseCreator(entityDictionary); 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 +64,6 @@ export class TypeScriptToFamixProcessor { this.currentCC = {}; } - this.setCurrentSourceFileName(file.getFilePath()); this.processFile(file); }); } @@ -105,18 +72,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.ensureFamixFile(f); logger.debug(`processFile: file: ${f.getBaseName()}, fqn = ${fmxFile.fullyQualifiedName}`); this.processComments(f, fmxFile); @@ -135,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}`); @@ -277,6 +234,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 +266,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 +789,67 @@ 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.importClauseCreator.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:`); modules.forEach(module => { - const modulePath = module.getFilePath(); + 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 => { - 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.importClauseCreator.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.importClauseCreator.ensureFamixImportClauseForDefaultImport( + impDecl, + 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.importClauseCreator.ensureFamixImportClauseForNamespaceImport( + namespaceImport, module, module + ); } }); }); } - - 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..88bd0afe 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,17 +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, 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 ensureFamixFile(f: SourceFile): Famix.ScriptEntity | Famix.Module { + const mapToFamixElement = (f: SourceFile) => { + let fmxFile: Famix.ScriptEntity | Famix.Module; + + const fileName = f.getBaseName(); + const isModule = isSourceFileAModule(f); 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(); @@ -240,20 +233,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 + ); } /** @@ -261,32 +246,20 @@ export class EntityDictionary { * @param moduleDeclaration A module * @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); + public ensureFamixModule(moduleDeclaration: ModuleDeclaration): Famix.Module { + 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 + ); } /** @@ -351,7 +324,7 @@ export class EntityDictionary { ); } - private ensureFamixElement< + public ensureFamixElement< TTMorphNode extends Node, TFamixElement extends Famix.SourcedEntity>( node: TTMorphNode, @@ -1385,118 +1358,6 @@ 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; - - 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}`); - - if (importDeclaration instanceof ImportDeclaration - && importElement instanceof ImportSpecifier) { - importedEntityName = importElement.getName(); - pathName = pathName + importedEntityName; - if (isInExports) { - importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(pathName) as Famix.NamedEntity; - logger.debug(`Found exported entity: ${pathName}`); - } - if (importedEntity === undefined) { - importedEntity = new Famix.NamedEntity(); - 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}`); - } - } - 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; - 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); - } - } - /** * Creates a Famix Arrow Function * @param arrowExpression An Expression @@ -1889,17 +1750,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/ImportClauseCreator.ts b/src/famix_functions/ImportClauseCreator.ts new file mode 100644 index 00000000..a148a955 --- /dev/null +++ b/src/famix_functions/ImportClauseCreator.ts @@ -0,0 +1,229 @@ +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 | Identifier, + 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 { + // 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( + importDeclaration: ImportDeclaration, defaultImport: Identifier, module: SourceFile + ) { + const namedEntityDeclaration = getDeclarationFromImportOrExport(defaultImport); + const moduleSpecifier = this.getModuleSpecifierFromDeclaration(importDeclaration); + + // TODO: finish implementation + 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 + + // 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; + }; + + 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 { + 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( + moduleSpecifierFileName ?? '', + 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) { + // 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; + } + + 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 + // 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); + } + }; + + 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 + // 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); + } + }; + + 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 c7a4d8e7..575d5d92 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, Identifier, ImportSpecifier, + InterfaceDeclaration, ModuleDeclaration, Node, SourceFile, SyntaxKind, ts } from "ts-morph"; import { Symbol as TSMorphSymbol } from "ts-morph"; /** @@ -27,6 +28,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 { @@ -90,3 +103,23 @@ function resolveSymbolToInterfaceOrClassDeclaration(symbol: TSMorphSymbol): Inte } return undefined; } + +export const getDeclarationFromImportOrExport = (importOrExport: ImportSpecifier | ExportSpecifier | Identifier): 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/famix_functions/famixIndexFileAnchorHelper.ts b/src/helpers/famixIndexFileAnchorHelper.ts similarity index 91% rename from src/famix_functions/famixIndexFileAnchorHelper.ts rename to src/helpers/famixIndexFileAnchorHelper.ts index 8fde51cd..2325704e 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,4 @@ export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePa pathInProject = pathInProject.substring(1); } return pathInProject; -}; \ No newline at end of file +}; diff --git a/src/helpers/incrementalUpdateHelper.ts b/src/helpers/incrementalUpdateHelper.ts new file mode 100644 index 00000000..f9846ee7 --- /dev/null +++ b/src/helpers/incrementalUpdateHelper.ts @@ -0,0 +1,120 @@ +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 { getFamixIndexFileAnchorFileName } from './famixIndexFileAnchorHelper'; +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, + allSourceFiles: SourceFile[], + projectBaseUrl: string +) => { + const sourceFilesToEnsureEntities = [ + ...(sourceFileChangeMap.get(SourceFileChangeType.Create) || []), + ...(sourceFileChangeMap.get(SourceFileChangeType.Update) || []), + ]; + + const dependentFileNames = getDependentSourceFileNames(dependentAssociations); + const dependentFileNamesToAdd = Array.from(dependentFileNames) + .map(fileName => getFamixIndexFileAnchorFileName(fileName, projectBaseUrl)) + .filter( + fileName => !Array.from(sourceFileChangeMap.values()) + .flat().some(sourceFile => sourceFile.getFilePath() === fileName)); + + const dependentFiles = allSourceFiles.filter( + sourceFile => { + const filePath = getFamixIndexFileAnchorFileName(sourceFile.getFilePath(), projectBaseUrl); + return dependentFileNamesToAdd.includes(filePath); + } + ); + + 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; +}; + +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[] = []; + + 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.superclass.removeSubInheritance(association); + association.subclass.removeSuperInheritance(association); + } else if (association instanceof ImportClause) { + association.importedEntity.incomingImports.delete(association); + association.importingEntity.outgoingImports.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/helpers/transientDependencyResolverHelper.ts b/src/helpers/transientDependencyResolverHelper.ts new file mode 100644 index 00000000..3a753756 --- /dev/null +++ b/src/helpers/transientDependencyResolverHelper.ts @@ -0,0 +1,68 @@ +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 [] +) => { + 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 1925ed54..b801eb69 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 @@ -95,58 +96,30 @@ 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 } } + // 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[]; + } + + public getInheritances(): Famix.Inheritance[] { + return Array.from(this.elements.values()).filter(e => e instanceof Famix.Inheritance) as Famix.Inheritance[]; + } // Only for tests 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/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/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/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"); - }); - -}); diff --git a/test/importClauseDefaultExports.test.ts b/test/importClauseDefaultExports.test.ts new file mode 100644 index 00000000..78d2b410 --- /dev/null +++ b/test/importClauseDefaultExports.test.ts @@ -0,0 +1,465 @@ +import { Class, ImportClause, Module, NamedEntity } 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('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); + 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('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 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('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); + }); + + 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(); + + 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('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'); + 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('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); + 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('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 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('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); + }); + + 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(); + + 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('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 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 = 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('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); + }); +}); \ 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..9ddc0ede --- /dev/null +++ b/test/importClauseNamedImport.test.ts @@ -0,0 +1,273 @@ +import { Class, ImportClause, Interface, 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 = 2; + 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 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/importClauseNamespaceImport.test.ts b/test/importClauseNamespaceImport.test.ts new file mode 100644 index 00000000..a82357fd --- /dev/null +++ b/test/importClauseNamespaceImport.test.ts @@ -0,0 +1,274 @@ +import { Class, ImportClause, Interface, Module, NamedEntity, 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('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); + expect(importClauses[0].importingEntity?.name).toBe('importingFile.ts'); + }); + + it("should work with namespace imports when exported class and interface", () => { + const importer = new Importer(); + const project = createProject(); + + project.createSourceFile("/exportingFile.ts", + `export class Helper {} + export interface Utils {} + `); + + project.createSourceFile("/importingFile.ts", + `import * as MyUtils from './exportingFile'; + `); + + 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_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 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'); + }); + + 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 = 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('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].importingEntity?.name).toBe('importingFile.ts'); + }); + + 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 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", + `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 = 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('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 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/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..710ec15f --- /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('Change import clause between 2 files', () => { + 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/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 new file mode 100644 index 00000000..187a2f75 --- /dev/null +++ b/test/incremental-update/associations/importClauseReExport.test.ts @@ -0,0 +1,596 @@ +import { expectRepositoriesToHaveSameStructure } from "../incrementalUpdateExpect"; +import { IncrementalUpdateProjectBuilder } from "../incrementalUpdateProjectBuilder"; +import { createExpectedFamixModelForSeveralFiles, getUpdateFileChangesMap } from "../incrementalUpdateTestHelper"; + +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} { } + `; + + 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); + }); +}); + +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'; + + 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 { } + `; + 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); + }); +}); + +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); + }); +}); + +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); + }); + + 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', () => { + 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 diff --git a/test/incremental-update/associations/modulesInheritance.test.ts b/test/incremental-update/associations/modulesInheritance.test.ts index 3a3f61b3..1865e065 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([ @@ -231,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 diff --git a/test/incremental-update/incrementalUpdateExpect.ts b/test/incremental-update/incrementalUpdateExpect.ts index 36d7b995..3db56f4f 100644 --- a/test/incremental-update/incrementalUpdateExpect.ts +++ b/test/incremental-update/incrementalUpdateExpect.ts @@ -1,12 +1,21 @@ -import { FamixBaseElement, Inheritance } from "../../src"; +import { FamixBaseElement, ImportClause, Inheritance, Module, NamedEntity, ScriptEntity } 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 && + actualAsNamedEntity.isStub === expectedAsNamedEntity.isStub; +}; + 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 +32,39 @@ 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 scriptEntityCompareFunction(actualAsModule, expectedAsModule) && + actualAsModule.isAmbient === expectedAsModule.isAmbient && + actualAsModule.isNamespace === expectedAsModule.isNamespace && + actualAsModule.isModule === expectedAsModule.isModule && + 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) => { @@ -46,14 +86,17 @@ export const expectRepositoriesToHaveSameStructure = (actual: FamixRepository, e expectElementsToBeEqualSize(actual, expected, "Enum"); expectElementsToBeEqualSize(actual, expected, "Function"); expectElementsToBeEqualSize(actual, expected, "ImportClause"); - // expectElementsToBeEqualSize(actual, expected, "IndexedFileAnchor"); + expectElementsToBeSame(actual, expected, "ImportClause", importClauseCompareFunction); + 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"); 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 +110,8 @@ 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"); + expectElementsToBeSame(actual, expected, "ScriptEntity", scriptEntityCompareFunction); expectElementsToBeEqualSize(actual, expected, "SourceAnchor"); expectElementsToBeEqualSize(actual, expected, "SourceLanguage"); expectElementsToBeEqualSize(actual, expected, "SourcedEntity"); @@ -75,7 +119,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) => { 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", `