Skip to content
Merged

Dev #66

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
54 changes: 54 additions & 0 deletions .github/workflows/vscode-extension-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: VS Code Extension CI

on:
push:
branches: [ master, dev, feature/add-tests ]
pull_request:
branches: [ master, dev ]
workflow_dispatch:

jobs:
test-vscode-extension:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
node-version: [20.x]

name: Test VS Code Extension on ${{ matrix.os }}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies for the ts2famix project
run: npm ci

- name: Build the ts2famix project
run: npm run build

- name: Install dependencies for the VS Code extension
working-directory: ./vscode-extension
run: npm install

- name: Run VS Code extension tests (Linux)
uses: coactions/setup-xvfb@v1
if: runner.os == 'Linux'
with:
run: |
echo "Running tests on Linux with virtual display"
npm test
working-directory: ./vscode-extension

- name: Run VS Code extension tests (Windows/macOS)
if: runner.os != 'Linux'
working-directory: ./vscode-extension
run: npm test
31 changes: 31 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug ts2famix-cli",
"program": "${workspaceFolder}/src/ts2famix-cli.ts",
"runtimeArgs": [
"-r",
"ts-node/register"
],
// NOTE: change this to your tsconfig.json path and output file path
"args": [
"--input",
"<your-path-here/tsconfig.json>",
"--output",
"${workspaceFolder}/<your-path-here/file-name.json>"
],
"cwd": "${workspaceFolder}",
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
]
},
]
}
6 changes: 5 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
"^.+\\.tsx?$": ["ts-jest", { }]
},
"testEnvironment": "jest-environment-node",
"verbose": true
"verbose": true,
"testPathIgnorePatterns": [
"/node_modules/",
"/vscode-extension/"
]
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"bin": {
"ts2famix": "dist/ts2famix-cli-wrapper.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"keywords": [
"TypeScript",
"famix",
Expand Down
121 changes: 78 additions & 43 deletions src/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { Project } from "ts-morph";
import { Project, SourceFile } from "ts-morph";
import * as fs from 'fs';
import { FamixRepository } from "./lib/famix/famix_repository";
import { Logger } from "tslog";
import * as processFunctions from "./analyze_functions/process_functions";
import { EntityDictionary } from "./famix_functions/EntityDictionary";
import { EntityDictionary, EntityDictionaryConfig } from "./famix_functions/EntityDictionary";
import path from "path";
import { TypeScriptToFamixProcessingContext } from "./analyze_functions/process_functions";
import { getFamixIndexFileAnchorFileName } from "./helpers";
import { isSourceFileAModule } from "./famix_functions/helpersTsMorphElementsProcessing";
import { FamixBaseElement } from "./lib/famix/famix_base_element";
import { getDirectDependentAssociations, getSourceFilesToUpdate, removeDependentAssociations } from "./helpers";
import { getTransientDependentEntities } from "./helpers/transientDependencyResolverHelper";

export const logger = new Logger({ name: "ts2famix", minLevel: 2 });
export const config = { "expectGraphemes": false };
export const entityDictionary = new EntityDictionary();

export enum SourceFileChangeType {
Create = 0,
Update = 1,
Delete = 2,
}

/**
* This class is used to build a Famix model from a TypeScript source code
*/
export class Importer {
private entityDictionary: EntityDictionary;
private typeScriptToFamixProcessingContext: TypeScriptToFamixProcessingContext ;

private project = new Project(
{
Expand All @@ -23,51 +34,46 @@ export class Importer {
}
); // The project containing the source files to analyze

constructor(config: EntityDictionaryConfig = { expectGraphemes: false }) {
this.entityDictionary = new EntityDictionary(config);
this.typeScriptToFamixProcessingContext = new TypeScriptToFamixProcessingContext (this.entityDictionary);
}

/**
* Main method
* @param paths An array of paths to the source files to analyze
* @returns The Famix repository containing the Famix model
*/
public famixRepFromPaths(paths: Array<string>): FamixRepository {

// try {
logger.debug(`famixRepFromPaths: paths: ${paths}`);

this.project.addSourceFilesAtPaths(paths);

initFamixRep(this.project);
this.initFamixRep(this.project);

this.processEntities(this.project);

const famixRep = entityDictionary.famixRep;
// }
// catch (error) {
// logger.error(`> ERROR: got exception ${error}. Exiting...`);
// logger.error(error.message);
// logger.error(error.stack);
// process.exit(1);
// }
const famixRep = this.entityDictionary.famixRep;

return famixRep;
}

private processEntities(project: Project): void {
const onlyTypeScriptFiles = project.getSourceFiles().filter(f => f.getFilePath().endsWith('.ts'));
processFunctions.processFiles(onlyTypeScriptFiles);
const accesses = processFunctions.accessMap;
const methodsAndFunctionsWithId = processFunctions.methodsAndFunctionsWithId;
const classes = processFunctions.classes;
const interfaces = processFunctions.interfaces;
const modules = processFunctions.modules;
const exports = processFunctions.listOfExportMaps;

processFunctions.processImportClausesForImportEqualsDeclarations(project.getSourceFiles(), exports);
processFunctions.processImportClausesForModules(modules, exports);
processFunctions.processAccesses(accesses);
processFunctions.processInvocations(methodsAndFunctionsWithId);
processFunctions.processInheritances(classes, interfaces);
processFunctions.processConcretisations(classes, interfaces, methodsAndFunctionsWithId);
this.typeScriptToFamixProcessingContext.processFiles(onlyTypeScriptFiles);

this.processReferences(onlyTypeScriptFiles, onlyTypeScriptFiles);
}

private processReferences(sourceFiles: SourceFile[], allExistingSourceFiles: SourceFile[]): void {
// TODO: we may process Invocations, Concretisations here if they are not processed when processing methods/functions, classes/interfaces and modules
this.typeScriptToFamixProcessingContext.processImportClausesForImportEqualsDeclarations(allExistingSourceFiles);

const modules = sourceFiles.filter(f => isSourceFileAModule(f));
this.typeScriptToFamixProcessingContext.processImportClausesForModules(modules);
// this.processFunctions.Invocations();
// etc.
}

/**
Expand All @@ -94,27 +100,56 @@ export class Importer {
* @returns The Famix repository containing the Famix model
*/
public famixRepFromProject(project: Project): FamixRepository {
//const sourceFileNames = project.getSourceFiles().map(f => f.getFilePath()) as Array<string>;

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

initFamixRep(project);
this.project = project;
this.initFamixRep(project);

this.processEntities(project);

return entityDictionary.famixRep;
return this.entityDictionary.famixRep;
}

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

function initFamixRep(project: Project): void {
// get compiler options
const compilerOptions = project.getCompilerOptions();
const removedEntities: FamixBaseElement[] = [];
allChangedSourceFiles.forEach(
file => {
const filePath = getFamixIndexFileAnchorFileName(file.getFilePath(), this.entityDictionary.getAbsolutePath());
const removed = this.entityDictionary.famixRep.removeEntitiesBySourceFile(filePath);
removedEntities.push(...removed);
}
);

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

const associationsToRemove = [...directDependentAssociations, ...transientDependentAssociations];

removeDependentAssociations(this.entityDictionary.famixRep, associationsToRemove);

// get baseUrl
const baseUrl = compilerOptions.baseUrl || ".";
const sourceFilesToEnsure = getSourceFilesToUpdate(
associationsToRemove, sourceFileChangeMap, allSourceFiles, this.entityDictionary.getAbsolutePath()
);

const absoluteBaseUrl = path.resolve(baseUrl);
this.typeScriptToFamixProcessingContext.processFiles(sourceFilesToEnsure);
const sourceFilesToDelete = sourceFileChangeMap.get(SourceFileChangeType.Delete) || [];
const existingSourceFiles = allSourceFiles.filter(
file => !sourceFilesToDelete.includes(file)
);
this.processReferences(sourceFilesToEnsure, existingSourceFiles);

entityDictionary.famixRep.setAbsolutePath(path.normalize(absoluteBaseUrl));
}

private initFamixRep(project: Project): void {
// get compiler options
const compilerOptions = project.getCompilerOptions();

// get baseUrl
const baseUrl = compilerOptions.baseUrl || ".";

const absoluteBaseUrl = path.resolve(baseUrl);

this.entityDictionary.setAbsolutePath(path.normalize(absoluteBaseUrl));
}
}
Loading
Loading