Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "ts2famix",
"version": "3.1.0",
"description": "A TypeScript to JSON importer for Moose.",
"main": "dist/ts2famix-cli.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "ts-node src/ts2famix-cli.ts",
"debug": "node --inspect-brk node_modules/.bin/ts-node",
Expand Down Expand Up @@ -66,5 +67,9 @@
},
"engines": {
"node": ">=18.20.4"
},
"exports": {
".": "./dist/index.js",
"./cli": "./dist/ts2famix-cli.js"
}
}
46 changes: 44 additions & 2 deletions src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import { Logger } from "tslog";
import * as processFunctions from "./analyze_functions/process_functions";
import { EntityDictionary } from "./famix_functions/EntityDictionary";
import path from "path";
import { SourceFile } from "ts-morph";
import { FamixBaseElement } from "./lib/famix/famix_base_element";
import { getFamixIndexFileAnchorFileName, getDirectDependentAssociations, getSourceFilesToUpdate, removeDependentAssociations } from "./helpers";
import { getTransientDependentEntities } from "./helpers/transientDependencyResolverHelper";


export enum SourceFileChangeType {
Create = 0,
Update = 1,
Delete = 2,
}
export const logger = new Logger({ name: "ts2famix", minLevel: 2 });
export const config = { "expectGraphemes": false };
export const entityDictionary = new EntityDictionary();
Expand Down Expand Up @@ -95,15 +105,47 @@ export class Importer {
*/
public famixRepFromProject(project: Project): FamixRepository {
//const sourceFileNames = project.getSourceFiles().map(f => f.getFilePath()) as Array<string>;

entityDictionary.reset();
//const famixRep = this.famixRepFromPaths(sourceFileNames);

processFunctions.resetProcessFunctions();
initFamixRep(project);

this.processEntities(project);

return entityDictionary.famixRep;
}
public updateFamixModelIncrementally(sourceFileChangeMap: Map<SourceFileChangeType, SourceFile[]>): void {
const allChangedSourceFiles = Array.from(sourceFileChangeMap.values()).flat();

const removedEntities: FamixBaseElement[] = [];
allChangedSourceFiles.forEach(file => {
const filePath = getFamixIndexFileAnchorFileName(file.getFilePath(), entityDictionary.getAbsolutePath());
const removed = entityDictionary.famixRep.removeEntitiesBySourceFile(filePath);
removedEntities.push(...removed);
});

const allSourceFiles = this.project.getSourceFiles();
const directDependentAssociations = getDirectDependentAssociations(removedEntities);
const transientDependentAssociations = getTransientDependentEntities(entityDictionary, sourceFileChangeMap);
const associationsToRemove = [...directDependentAssociations, ...transientDependentAssociations];

removeDependentAssociations(entityDictionary.famixRep, associationsToRemove);

const sourceFilesToEnsure = getSourceFilesToUpdate(
associationsToRemove, sourceFileChangeMap, allSourceFiles, entityDictionary.getAbsolutePath()
);

processFunctions.processFiles(sourceFilesToEnsure);
const sourceFilesToDelete = sourceFileChangeMap.get(SourceFileChangeType.Delete) || [];
const existingSourceFiles = allSourceFiles.filter(file => !sourceFilesToDelete.includes(file));
this.processReferences(sourceFilesToEnsure, existingSourceFiles);
}
private processReferences(sourceFiles: SourceFile[], allExistingSourceFiles: SourceFile[]): void {
const allSourceFilesArray = Array.from(allExistingSourceFiles);
processFunctions.processImportClausesForImportEqualsDeclarations(allSourceFilesArray, processFunctions.listOfExportMaps);
const modules = sourceFiles.filter(f => processFunctions.isSourceFileAModule(f));
processFunctions.processImportClausesForModules(modules, processFunctions.listOfExportMaps);
}

}

Expand Down
11 changes: 10 additions & 1 deletion src/analyze_functions/process_functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ export const listOfExportMaps = new Array<ReadonlyMap<string, ExportedDeclaratio
export let currentCC: { [key: string]: number }; // Stores the cyclomatic complexity metrics for the current source file
const processedNodesWithTypeParams = new Set<number>(); // Set of nodes that have been processed and have type parameters

export function resetProcessFunctions(): void {
methodsAndFunctionsWithId.clear();
accessMap.clear();
classes.length = 0;
interfaces.length = 0;
modules.length = 0;
listOfExportMaps.length = 0;
}

/**
* 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 {
export function isSourceFileAModule(sourceFile: SourceFile): boolean {
return sourceFile.getImportDeclarations().length > 0 || sourceFile.getExportedDeclarations().size > 0;
}

Expand Down
24 changes: 23 additions & 1 deletion src/famix_functions/EntityDictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,29 @@ export class EntityDictionary {
constructor() {
this.famixRep.setFmxElementObjectMap(this.fmxElementObjectMap);
}

public reset(): void {
this.famixRep = new FamixRepository();
this.fmxElementObjectMap = new Map();
this.tsMorphElementObjectMap = new Map();
this.fmxAliasMap = new Map();
this.fmxClassMap = new Map();
this.fmxInterfaceMap = new Map();
this.fmxModuleMap = new Map();
this.fmxFileMap = new Map();
this.fmxTypeMap = new Map();
this.fmxPrimitiveTypeMap = new Map();
this.fmxFunctionAndMethodMap = new Map();
this.fmxArrowFunctionMap = new Map();
this.fmxParameterMap = new Map();
this.fmxVariableMap = new Map();
this.fmxImportClauseMap = new Map();
this.fmxEnumMap = new Map();
this.fmxInheritanceMap = new Map();
this.famixRep.setFmxElementObjectMap(this.fmxElementObjectMap);
}
public getAbsolutePath(): string {
return this.famixRep.getAbsolutePath();
}
public addSourceAnchor(fmx: Famix.SourcedEntity, node: TSMorphObjectType): Famix.IndexedFileAnchor {
const sourceAnchor: Famix.IndexedFileAnchor = new Famix.IndexedFileAnchor();
let sourceStart, sourceEnd: number;
Expand Down
12 changes: 12 additions & 0 deletions src/famix_functions/helpers_path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { logger } from "../analyze";

export function convertToRelativePath(absolutePath: string, absolutePathProject: string) {
logger.debug(`convertToRelativePath: absolutePath: '${absolutePath}', absolutePathProject: '${absolutePathProject}'`);
if (absolutePath.startsWith(absolutePathProject)) {
return absolutePath.replace(absolutePathProject, "").slice(1);
} else if (absolutePath.startsWith("/")) {
return absolutePath.slice(1);
} else {
return absolutePath;
}
}
24 changes: 24 additions & 0 deletions src/helpers/famixIndexFileAnchorHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { convertToRelativePath } from "../famix_functions/helpers_path";
import path from "path";

export const getFamixIndexFileAnchorFileName = (absolutePath: string, absolutePathProject: string) => {
absolutePath = path.normalize(absolutePath);
const positionNodeModules = absolutePath.indexOf('node_modules');

let pathInProject: string = "";

if (positionNodeModules !== -1) {
const pathFromNodeModules = absolutePath.substring(positionNodeModules);
pathInProject = pathFromNodeModules;
} else {
pathInProject = convertToRelativePath(absolutePath, absolutePathProject);
}

// revert any backslashes to forward slashes (path.normalize on windows introduces them)
pathInProject = pathInProject.replace(/\\/g, "/");

if (pathInProject.startsWith("/")) {
pathInProject = pathInProject.substring(1);
}
return pathInProject;
};
106 changes: 106 additions & 0 deletions src/helpers/incrementalUpdateHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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';

// TODO: add tests for these methods
export const getSourceFilesToUpdate = (
dependentAssociations: EntityWithSourceAnchor[],
sourceFileChangeMap: Map<SourceFileChangeType, SourceFile[]>,
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<string>();

dependentAssociations.forEach(entity => {
// todo: ? sourceAnchor instead of indexedfileAnchor
dependentFileNames.add((entity.sourceAnchor as IndexedFileAnchor).fileName);
});

return dependentFileNames;
};

/**
* Finds all the associations that include the given entities as dependencies
*/
export const getDirectDependentAssociations = (entities: FamixBaseElement[]) => {
const dependentAssociations: EntityWithSourceAnchor[] = [];

entities.forEach(entity => {
dependentAssociations.push(...getDependentAssociationsForEntity(entity));
});

return dependentAssociations;
};

const getDependentAssociationsForEntity = (entity: FamixBaseElement) => {
const dependentAssociations: EntityWithSourceAnchor[] = [];

const addElementFileToSet = (association: EntityWithSourceAnchor) => {
dependentAssociations.push(association);
};

if (entity instanceof Class) {
Array.from(entity.subInheritances).forEach(inheritance => {
addElementFileToSet(inheritance);
});
} else if (entity instanceof Interface) {
Array.from(entity.subInheritances).forEach(inheritance => {
addElementFileToSet(inheritance);
});
}

if (entity instanceof NamedEntity) {
Array.from(entity.incomingImports).forEach(importClause => {
addElementFileToSet(importClause);
});
}
// TODO: add other associations

return dependentAssociations;
};

export const removeDependentAssociations = (
famixRep: FamixRepository,
dependentAssociations: EntityWithSourceAnchor[]) => {
// NOTE: removing the depending associations because they will be recreated later
famixRep.removeElements(dependentAssociations);
famixRep.removeElements(dependentAssociations.map(x => x.sourceAnchor));

dependentAssociations.forEach(association => {
if (association instanceof Inheritance) {
association.superclass.removeSubInheritance(association);
association.subclass.removeSuperInheritance(association);
} else if (association instanceof ImportClause) {
association.importedEntity.incomingImports.delete(association);
association.importingEntity.outgoingImports.delete(association);
}
});
};
2 changes: 2 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './incrementalUpdateHelper';
export * from './famixIndexFileAnchorHelper';
104 changes: 104 additions & 0 deletions src/helpers/transientDependencyResolverHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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";
import { getFamixIndexFileAnchorFileName } from "./famixIndexFileAnchorHelper";
import { SourceFileChangeType } from "../analyze";
import { SourceFile } from "ts-morph";

// TODO: add tests for these methods

/**
* NOTE: for now the case when we create a new file and there were imports from it
* even if it didn't exist may not be working.
*
* Ex.,:
* fileA: *does not exists yet*
* fileB: import { Something } from './fileA';
* ------------------------
* fileA: export class Something { }
*
* (the fileB may not be updated here)
*/

/**
* Based on import clauses finds the dependent files and returns the associations
* that are transitively dependent on the changed files. It does it recursively.
*/
export const getTransientDependentEntities = (
entityDictionary: EntityDictionary,
sourceFileChangeMap: Map<SourceFileChangeType, SourceFile[]>,
) => {
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 getTransientDependentAssociations = (
entityDictionary: EntityDictionary,
changedFilesNames: string []
) => {
const importClauses = entityDictionary.famixRep.getImportClauses();

const transientDependentAssociations: Set<EntityWithSourceAnchor> = new Set();

const unprocessedFiles: Set<string> = new Set(changedFilesNames);
const processedFiles: Set<string> = 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<EntityWithSourceAnchor>
) => {
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 (access, invocation) between the imported entity and the sourceFile
};
Loading
Loading