Skip to content
Merged
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
71 changes: 64 additions & 7 deletions clean-architecture-visualizer/src/data_access/fileAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,92 @@ export class FileAccess implements FileAccessInterface {

/**
* Read the imports of the file that path points to and return a list of module names.
* Collects normal imports first before collecting package imports.
* @param filePath is a path to a valid file.
*/
async getFileImports(filePath: string): Promise<string[]> {
let result: string[] = [];
const result: string[] = [];

try {
const fileContent: string = await fs.readFile(filePath, {
encoding: 'utf-8',
});
const fileLines = fileContent.split('\n');
fileLines.forEach((line) => {

// package is always the first non-empty line (Package Imports in Java only)
let packageSet: Set<string> | null = null;
for (const line of fileLines) {
const trimmed_line = line.trim();
if (trimmed_line === '') continue;
if (trimmed_line.startsWith('package ')) {
const packageDir = filePath.substring(
0,
filePath.lastIndexOf('/') + 1
); // package dir is always one dir above filepath
const files = await fs.readdir(packageDir);
packageSet = new Set<string>();
const currentFileName = filePath.split('/').at(-1) ?? '';
for (const file of files) {
if (file !== currentFileName) {
packageSet.add(file.replace(/\.[^.]+$/, '')); // replaces everything after the dot so fileAccess.ts -> fileAccess
}
}
}
break;
}
for (const line of fileLines) {
if (
line.startsWith('import ') ||
line.startsWith('from ') ||
line.startsWith('import{')
) {
line = line.trim();
const lastSpace = line.lastIndexOf(' ');
result.push(line.substring(lastSpace + 1));
const trimmed_line = line.trim();
const lastSpace = trimmed_line.lastIndexOf(' ');
result.push(trimmed_line.substring(lastSpace + 1));
}
});
}
// If package detected, with files other than the current file, then iterate through entire file
if (packageSet) {
const packageImports = this.getPackageImports(fileLines, packageSet);
result.push(...packageImports); // pushed depenedency files are stripped of extra details, pushes LoginInputData not '"LoginInputData";'
}
} catch {
console.log('The file: ' + filePath + ' could not be found');
return [];
}

return result;
}

/**
* Scan file lines for usages of sibling class names from the same package.
* @param fileLines the lines of the file to scan.
* @param packageSet set of class names (without extension) in the same package.
* @returns list of class names from the package that are used in the file.
*/
private getPackageImports(
fileLines: string[],
packageSet: Set<string>
): string[] {
const found = new Set<string>();
for (const line of fileLines) {
const trimmed_line = line.trim();
if (
trimmed_line.startsWith('import ') ||
trimmed_line.startsWith('package ') ||
trimmed_line === ''
) {
continue;
}
for (const className of packageSet) {
if (!found.has(className) && trimmed_line.includes(className)) {
found.add(className);
}
}
if (found.size === packageSet.size) break;
}
return [...found];
}

/**
* Get the project name, this is either the directory BEFORE "src", or if the
* process is running in a directory ABOVE "src" we assume that we are in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,33 @@ describe('getFileImports functionality', () => {
expect(result).toEqual(['"fs/promises";', '"path";']);
});

it('returns package imports not specified at by an import command', async () => {
mockReaddir.mockResolvedValueOnce([
'LoginInputBoundary.java',
'LoginInputData.java',
] as any);
mockReadFile.mockResolvedValueOnce(
'package use_case.login;\nfinal int x = 5\nLoginInputData output = new LoginOutputData()'
);
const result = await fileAccess.getFileImports('/project/index.ts');
expect(result).toEqual(['LoginInputData']);
});

it('returns both package imports and normal imports', async () => {
mockReaddir.mockResolvedValueOnce([
'LoginInputBoundary.java',
'LoginInputData.java',
'LoginInteractor.java',
] as any);
mockReadFile.mockResolvedValueOnce(
'package use_case.login;\nimport entity.User;\npublic class LoginInteractor implements LoginInputBoundary{}'
);
const result = await fileAccess.getFileImports(
'/project/LoginInteractor.java'
);
expect(result).toEqual(['entity.User;', 'LoginInputBoundary']);
});

it('returns an empty array and logs when the file is not found', async () => {
mockReadFile.mockRejectedValueOnce(new Error('File not found') as any);
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
Expand Down
Loading