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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ clean-architecture-visualizer/frontend/public/

# Secrets
.DS_Store
.env
.env

# Local task tracking
tasks/
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Annie Wang,
Cindy Yang,
Ethan Diep,
Eric Xu,
Hiu Yan Kwok,
Jayden Ong
Matthew Dahlgren,
Olivier Denis,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { GetFilesWithViolationsInputBoundary } from '../../use_case/getFilesWithViolations/getFilesWithViolationsInputBoundary.js';

export class GetFilesWithViolationsController {
constructor(
private readonly inputboundary: GetFilesWithViolationsInputBoundary
) {}

async execute(): Promise<void> {
await this.inputboundary.execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { GetFilesWithViolationsOutputBoundary } from '../../use_case/getFilesWithViolations/getFilesWithViolationsOutputBoundary.js';
import type { GetFilesWithViolationsOutputData } from '../../use_case/getFilesWithViolations/getFilesWithViolationsOutputData.js';

export class GetFilesWithViolationsPresenter implements GetFilesWithViolationsOutputBoundary {
constructor(private readonly outputData: GetFilesWithViolationsOutputData) {}

getOutputData() {
return this.outputData.getOutputData();
}
}
16 changes: 16 additions & 0 deletions clean-architecture-visualizer/src/server/routes/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { GetViolationsInteractor } from '../../use_case/getViolations/GetViolati
import { FileAccess } from '../../data_access/fileAccess.js';
import { GetViolationsController } from '../../interface_adapter/getViolations/getViolationsController.js';
import { GetViolationsPresenter } from '../../interface_adapter/getViolations/getViolationsPresenter.js';
import { GetFilesWithViolationsOutputData } from '../../use_case/getFilesWithViolations/getFilesWithViolationsOutputData.js';
import { GetFilesWithViolationsInteractor } from '../../use_case/getFilesWithViolations/getFilesWithViolationsInteractor.js';
import { GetFilesWithViolationsController } from '../../interface_adapter/getFilesWithViolations/getFilesWithViolationsController.js';
import { GetFilesWithViolationsPresenter } from '../../interface_adapter/getFilesWithViolations/getFilesWithViolationsPresenter.js';

const router = Router();

Expand Down Expand Up @@ -89,4 +93,16 @@ router.get('/analysis/violations/:interactionId', async (req, res) => {
res.json(result);
});

router.get('/analysis/files-with-violations', (_req, res) => {
const outputData = new GetFilesWithViolationsOutputData();
const interactor = new GetFilesWithViolationsInteractor(dbAccess, outputData);
const controller = new GetFilesWithViolationsController(interactor);
const presenter = new GetFilesWithViolationsPresenter(outputData);

controller.execute();
const result = presenter.getOutputData();

res.json(result);
});

export default router;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GetFilesWithViolationsInputBoundary {
execute(): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { GetFilesWithViolationsInputBoundary } from './getFilesWithViolationsInputBoundary.js';
import type { SessionDBAccessInterface } from '../../data_access/sessionDBAccessInterface.js';
import type { GetFilesWithViolationsOutputData } from './getFilesWithViolationsOutputData.js';

export class GetFilesWithViolationsInteractor implements GetFilesWithViolationsInputBoundary {
constructor(
private readonly db: SessionDBAccessInterface,
private readonly outputdata: GetFilesWithViolationsOutputData
) {}

async execute(): Promise<void> {
const violatingNodes = this.db.getNodesByStatus('VIOLATION');

const violatingNodesPaths = violatingNodes.map((node) => node.filePath);

const filteredViolatingNodePaths = violatingNodesPaths.filter(
(path): path is string => path != undefined
);

const uniqueNodesPaths = [...new Set(filteredViolatingNodePaths)];

this.outputdata.setOutputData({
total_violations: violatingNodes.length,
files: uniqueNodesPaths,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GetFilesWithViolationsOutputBoundary {
getOutputData(): object;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class GetFilesWithViolationsOutputData {
private filesWithViolationsOutputData?: { [key: string]: any };

setOutputData(OutputData: { [key: string]: any }) {
this.filesWithViolationsOutputData = OutputData;
}

getOutputData(): object {
if (this.filesWithViolationsOutputData)
return this.filesWithViolationsOutputData;
return {};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
import { GetFilesWithViolationsInteractor } from '../../../src/use_case/getFilesWithViolations/getFilesWithViolationsInteractor.js';
import { SessionDBAccess } from '../../../src/data_access/sessionDBAccess.js';
import type { GetFilesWithViolationsOutputData } from '../../../src/use_case/getFilesWithViolations/getFilesWithViolationsOutputData.js';

const genericDBAccess = new SessionDBAccess();

function makeOutputData(): GetFilesWithViolationsOutputData & { result: any } {
return {
result: undefined,
setOutputData(data: any) {
this.result = data;
},
getOutputData() {
return this.result;
},
} as GetFilesWithViolationsOutputData & { result: any };
}

describe('GetFilesWithViolationsInteractor', () => {
beforeEach(() => {
genericDBAccess.resetDB();
});

it('returns the file paths of nodes with VIOLATION status', async () => {
genericDBAccess.upsertNode({
id: 'node-1',
type: 'entities',
layer: 'enterpriseBusinessRules',
filePath: 'src/entities/Student.java',
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-2',
type: 'useCaseInteractor',
layer: 'applicationBusinessRules',
filePath: 'src/use_case/EnrollStudent.java',
status: 'VIOLATION',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.files).toEqual(
expect.arrayContaining([
'src/entities/Student.java',
'src/use_case/EnrollStudent.java',
])
);
expect(outputData.result.files).toHaveLength(2);
});

it('excludes nodes that are not in VIOLATION status', async () => {
genericDBAccess.upsertNode({
id: 'node-bad',
type: 'controller',
layer: 'interfaceAdapters',
filePath: 'src/interface_adapters/BadController.java',
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-valid',
type: 'controller',
layer: 'interfaceAdapters',
filePath: 'src/interface_adapters/GoodController.java',
status: 'VALID',
});
genericDBAccess.upsertNode({
id: 'node-missing',
type: 'presenter',
layer: 'interfaceAdapters',
status: 'MISSING',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.files).toEqual([
'src/interface_adapters/BadController.java',
]);
});

it('deduplicates file paths when multiple violation nodes share a file', async () => {
const filePath = 'src/use_case/EnrollStudent.java';

genericDBAccess.upsertNode({
id: 'node-1',
type: 'useCaseInteractor',
layer: 'applicationBusinessRules',
filePath: filePath,
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-2',
type: 'inputBoundary',
layer: 'applicationBusinessRules',
filePath: filePath,
status: 'VIOLATION',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.files).toEqual([filePath]);
});

it('filters out violation nodes that have no file path', async () => {
genericDBAccess.upsertNode({
id: 'node-real',
type: 'entities',
layer: 'enterpriseBusinessRules',
filePath: 'src/entities/Student.java',
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-missing',
type: 'controller',
layer: 'interfaceAdapters',
status: 'VIOLATION',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.files).toEqual(['src/entities/Student.java']);
});

it('returns an empty files array when there are no violations', async () => {
genericDBAccess.upsertNode({
id: 'node-valid',
type: 'controller',
layer: 'interfaceAdapters',
filePath: 'src/interface_adapters/GoodController.java',
status: 'VALID',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.files).toEqual([]);
expect(outputData.result.total_violations).toBe(0);
});

it('counts total_violations by node, not by unique file', async () => {
const filePath = 'src/use_case/EnrollStudent.java';

genericDBAccess.upsertNode({
id: 'node-1',
type: 'useCaseInteractor',
layer: 'applicationBusinessRules',
filePath: filePath,
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-2',
type: 'inputBoundary',
layer: 'applicationBusinessRules',
filePath: filePath,
status: 'VIOLATION',
});
genericDBAccess.upsertNode({
id: 'node-3',
type: 'controller',
layer: 'interfaceAdapters',
status: 'VIOLATION',
});

const outputData = makeOutputData();
const interactor = new GetFilesWithViolationsInteractor(
genericDBAccess,
outputData
);

await interactor.execute();

expect(outputData.result.total_violations).toBe(3);
expect(outputData.result.files).toHaveLength(1);
});
});
Loading