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
19 changes: 16 additions & 3 deletions clean-architecture-visualizer/src/data_access/fileAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,15 @@ export class FileAccess implements FileAccessInterface {
return importLines.length > 0 ? importLines.join('\n') : undefined;
}

const match = importLines.find((line) =>
line.toLowerCase().includes(target.toLowerCase())
// The getFileSnippet function is only called by the getViolationsInteractor which only ever passes in cleanNode
// If the cleanNode is "entities", we will also allow for "entity"
const match = importLines.find(
(line) =>
line.toLowerCase().includes(target.toLowerCase()) ||
(target.toLowerCase() === 'entities' &&
line.toLowerCase().includes('entity')) ||
(target.toLowerCase() === 'usecaseinteractor' &&
line.toLowerCase().includes('interactor'))
);

return match?.trim() ?? undefined;
Expand All @@ -274,11 +281,17 @@ export class FileAccess implements FileAccessInterface {

const lines = content.split('\n');

// The getLineNumber function is only called by the getViolationsInteractor which only ever passes in cleanNode
// If the cleanNode is "entities", we will also allow for "entity"
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (
line.trimStart().startsWith('import ') &&
line.toLowerCase().includes(target.toLowerCase())
(line.toLowerCase().includes(target.toLowerCase()) ||
(target.toLowerCase() === 'entities' &&
line.toLowerCase().includes('entity')) ||
(target.toLowerCase() === 'usecaseinteractor' &&
line.toLowerCase().includes('interactor')))
) {
return i + 1; // 1-based line number
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ export class GetViolationsInteractor implements GetViolationsInputBoundary {
useCaseName: string
): Promise<FileContext | undefined> {
const fileKeySet = new Set(fileKeys);

const matchingNode = this.db
// It is possible for multiple files to have the desired clean node and have the same file structure
const matchingNodes = this.db
.getAllNodes()
.find(
.filter(
(n) =>
n.type === from &&
n.filePath !== undefined &&
Expand All @@ -122,19 +122,26 @@ export class GetViolationsInteractor implements GetViolationsInputBoundary {
(fileKeySet.has(n.filePath) &&
this.findNodeContainsUseCase(n.id, useCaseName)))
);
if (!matchingNode?.filePath) return undefined;
const fileName = matchingNode.filePath.split('/').at(-1);
if (!fileName) return undefined;
const [snippet, line_number] = await Promise.all([
this.fileAccess.getFileSnippet(matchingNode.filePath, to),
this.fileAccess.getLineNumber(matchingNode.filePath, to),
]);

return {
file: fileName,
...(snippet && { snippet }),
...(line_number && { line_number }),
};
if (!matchingNodes) return undefined;
for (const matchingNode of matchingNodes) {
const fileName = matchingNode.filePath?.split('/').at(-1);
if (!fileName) return undefined;
const [snippet, line_number] = await Promise.all([
this.fileAccess.getFileSnippet(matchingNode.filePath as string, to),
this.fileAccess.getLineNumber(matchingNode.filePath as string, to),
]);

if (snippet && line_number) {
return {
file: fileName,
...(snippet && { snippet }),
...(line_number && { line_number }),
};
}
}
// This SHOULD never run
return undefined;
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
let allEdges: string[][] = [];
// map use case graph names to graphs
let useCaseGraphNamesToGraph = new Map<string, useCaseGraph>();

let useCaseIndex = 0;
for (const graph of this.useCaseGraphList) {
this.crossUseCaseEdges.push([]);
Expand All @@ -151,11 +150,6 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
if (toNode) {
let importFileName = importPath.split('/').at(-1) ?? '';
importFileName = importFileName.split('.').at(0) ?? '';

const modifiedImportPath =
importPath.length > 0 && importPath.at(-1) === ';'
? importPath.slice(0, -1)
: importPath;
//Check if the imported file is an external file path
if (
!useCaseFiles.includes(importFileName) &&
Expand All @@ -165,16 +159,23 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
this.crossUseCaseFiles.add(filePath);
} else {
graph.setNodeNeighbour(fromNode, toNode);
if (this.externalFilePaths.has(modifiedImportPath)) {
graph.addFile(
modifiedImportPath,
this.externalFilePaths.get(modifiedImportPath) as string
);
// A problem we are having is the extensions between js and ts
// To get around that, we find the externalFileName (has extension) that has the importFileName (no extension)
const externalFileNamePath = [...this.externalFilePaths].find(
([externalFileName, _]) =>
externalFileName.includes(importFileName)
);
// Making the assumption that no two files are ever named the same
// this.externalFilePaths maps the name of the file to its filePath
// importFileName only contains the name of the file

if (externalFileNamePath) {
graph.addFile(externalFileNamePath[0], externalFileNamePath[1]);
// Gets the external file path from the modified import path and adds the use case graph name
// to the set of use case graphs that the external file belongs to
// Nothing to optimize.
externalFilesToUseCaseGraphs
.get(this.externalFilePaths.get(modifiedImportPath) as string)
.get(externalFileNamePath[1])
?.add(graph.getName());
}
}
Expand All @@ -195,6 +196,8 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
// There is probably room to optimize since we iterate #graphs * #imports * #internal_files
this.useCaseGraphList.map((graph) => {
useCaseGraphNamesToGraph.set(graph.getName(), graph);
// Want to add all external files to all use case graphs
graph.addFile(fileName, filePath);
imports.map((importPath) =>
[...graph.getFiles().keys()].map((targetFileName) => {
const base = targetFileName.toLowerCase().replace(/\.[^.]+$/, '');
Expand Down Expand Up @@ -227,10 +230,12 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
if (this.resolveImportToFileName(this.internalFilePaths, importPath)) {
return;
}

const targetFileName = this.resolveImportToFileName(
this.externalFilePaths,
importPath
);

if (!targetFileName) return;

const toFilePath = this.externalFilePaths.get(targetFileName) as string;
Expand All @@ -243,7 +248,6 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
);
});
}

allEdges.map(([fromNodePath, toNodePath]) => {
if (
toNodePath &&
Expand Down Expand Up @@ -358,7 +362,8 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
if (importPath.includes('viewmodel')) return 'viewModel'; // must be verified before 'view'
if (importPath.includes('view')) return 'view';
if (importPath.includes('database')) return 'database';
if (importPath.includes('entity')) return 'entities';
if (importPath.includes('entity') || importPath.includes('entities'))
return 'entities';
if (importPath.includes('accessinterface')) return 'dataAccessInterface'; // must be verified before 'dataAccess'
if (importPath.includes('access')) return 'dataAccess';
if (importPath.includes('controller')) return 'controller';
Expand Down Expand Up @@ -589,7 +594,6 @@ export class GraphVerificationInteractor implements GraphVerificationInputBounda
);

const neighbourMap = uc.getNeighbourMap();

for (const [fromNode, neighbours] of Object.entries(neighbourMap) as [
cleanNode,
cleanNode[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,33 @@ describe('getUseCases functionality', () => {
expect(result).toEqual([]);
});
});

describe('getFileSnippet functionality', () => {
const fileAccess = new FileAccess();

beforeEach(() => {
jest.clearAllMocks();
});

it('successfully retrieves file snippet from file', async () => {
mockReadFile.mockResolvedValueOnce(
'not a line\nimport ../usecase1InputBoundary.py;'
);
const res = await fileAccess.getFileSnippet('/src', 'inputBoundary');
expect(res).toBe('import ../usecase1InputBoundary.py;');
});

it('successfully retrieves file snippet from file when target is entity', async () => {
mockReadFile.mockResolvedValueOnce('import ../entities/entity1.py;');
const res = await fileAccess.getFileSnippet('/src', 'entity');
expect(res).toBe('import ../entities/entity1.py;');
});

it('successfully retrieves file snippet from file when target is use case interactor', async () => {
mockReadFile.mockResolvedValueOnce(
'import ../use_case/usecase1Interactor.py;'
);
const res = await fileAccess.getFileSnippet('/src', 'useCaseInteractor');
expect(res).toBe('import ../use_case/usecase1Interactor.py;');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,22 @@ describe('GetViolationsInteractor', () => {
status: 'VALID',
});

genericDBAccess.upsertNode({
id: 'src/interface_adapters/uc-1/User2Controller.java-Process User',
type: fromNode,
layer: 'interfaceAdapters',
filePath: 'src/interface_adapters/uc-1/User2Controller.java',
status: 'VALID',
});

// Setup the use case with a violation edge
const mockUseCase = {
id: 'uc-1',
name: 'Process User',
fileKeys: [filePath],
fileKeys: [
filePath,
'src/interface_adapters/uc-1/User2Controller.java',
],
violationEdges: [[fromNode, toNode]] as [string, string][],
};
(genericDBAccess as any).upsertUseCase(mockUseCase);
Expand Down Expand Up @@ -122,6 +133,42 @@ describe('GetViolationsInteractor', () => {
expect(context.line_number).toBe(mockLine);
});

it('populates file context with snippets and line numbers from FileAccess when first node is invalid', async () => {
const mockSnippet = 'import entity1.java;';
const mockLine = 5;

jest
.spyOn(genericFileAccess, 'getFileSnippet')
.mockImplementation(async (filePath, _) => {
if (filePath.includes('UserController.java')) {
return undefined;
}
return mockSnippet;
});
jest
.spyOn(genericFileAccess, 'getLineNumber')
.mockImplementation(async (filePath, _) => {
if (filePath.includes('UserController.java')) {
return undefined;
}
return mockLine;
});

const outputData = makeOutputData();
const interactor = new GetViolationsInteractor(
genericDBAccess,
genericFileAccess,
makeInputData('uc-1'),
outputData
);

await interactor.execute();
const context = outputData.result[0].file_context;
expect(context.file).toBe('User2Controller.java');
expect(context.snippet).toBe(mockSnippet);
expect(context.line_number).toBe(mockLine);
});

it('handles missing file context gracefully if no matching node exists', async () => {
// Reset DB and add use case without matching nodes
genericDBAccess.resetDB();
Expand Down
Loading
Loading