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
515 changes: 515 additions & 0 deletions CODE_ANALYSIS_REPORT.md

Large diffs are not rendered by default.

838 changes: 838 additions & 0 deletions TEST_COVERAGE_PLAN.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/lang/ArchRuleDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,16 @@ export class ClassesShouldStatic {
);
}

/**
* Classes should NOT reside in a specific package
*/
public notResideInPackage(packagePattern: string): StaticArchRule {
return new StaticArchRule((classes) => {
const filtered = this.applyFilters(classes);
return new ClassesShould(filtered).notResideInPackage(packagePattern);
}, `Classes should not reside in package '${packagePattern}'`);
}

/**
* Classes should be annotated with a decorator
*/
Expand Down
110 changes: 110 additions & 0 deletions src/lang/syntax/ClassesShould.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export class ClassesShould {
return new PackageRule(this.tsClasses, packagePattern, true);
}

/**
* Classes should NOT reside in a specific package (alias for resideOutsideOfPackage)
*/
public notResideInPackage(packagePattern: string): ArchRule {
return new PackageRule(this.tsClasses, packagePattern, true);
}

/**
* Classes should be annotated with a decorator
*/
Expand Down Expand Up @@ -62,6 +69,34 @@ export class ClassesShould {
return new NamingRule(this.tsClasses, prefix, 'startingWith');
}

/**
* Classes should NOT have a specific simple name (exact match)
*/
public notHaveSimpleName(name: string): ArchRule {
return new NotSimpleNameRule(this.tsClasses, name);
}

/**
* Classes should NOT have names matching a pattern
*/
public notHaveSimpleNameMatching(pattern: RegExp | string): ArchRule {
return new NotNamingRule(this.tsClasses, pattern, 'matching');
}

/**
* Classes should NOT have names ending with a suffix
*/
public notHaveSimpleNameEndingWith(suffix: string): ArchRule {
return new NotNamingRule(this.tsClasses, suffix, 'endingWith');
}

/**
* Classes should NOT have names starting with a prefix
*/
public notHaveSimpleNameStartingWith(prefix: string): ArchRule {
return new NotNamingRule(this.tsClasses, prefix, 'startingWith');
}

/**
* Classes should only depend on classes in specific packages
*/
Expand Down Expand Up @@ -913,3 +948,78 @@ class AssignableFromRule extends BaseArchRule {
return violations;
}
}

/**
* Rule for checking that classes do NOT match naming patterns
*/
class NotNamingRule extends BaseArchRule {
constructor(
private classes: TSClasses,
private pattern: RegExp | string,
private type: 'matching' | 'endingWith' | 'startingWith'
) {
super(`Classes should not have simple name ${type} '${pattern}'`);
}

check(): ArchitectureViolation[] {
const violations: ArchitectureViolation[] = [];

for (const cls of this.classes.getAll()) {
let matches = false;

switch (this.type) {
case 'matching':
matches = cls.hasSimpleNameMatching(this.pattern);
break;
case 'endingWith':
matches = cls.hasSimpleNameEndingWith(this.pattern as string);
break;
case 'startingWith':
matches = cls.hasSimpleNameStartingWith(this.pattern as string);
break;
}

if (matches) {
violations.push(
this.createViolation(
`Class '${cls.name}' should not have simple name ${this.type} '${this.pattern}'`,
cls.filePath,
this.description
)
);
}
}

return violations;
}
}

/**
* Rule for checking that classes do NOT have a specific simple name (exact match)
*/
class NotSimpleNameRule extends BaseArchRule {
constructor(
private classes: TSClasses,
private name: string
) {
super(`Classes should not have simple name '${name}'`);
}

check(): ArchitectureViolation[] {
const violations: ArchitectureViolation[] = [];

for (const cls of this.classes.getAll()) {
if (cls.name === this.name) {
violations.push(
this.createViolation(
`Class '${cls.name}' should not have simple name '${this.name}'`,
cls.filePath,
this.description
)
);
}
}

return violations;
}
}
18 changes: 12 additions & 6 deletions test/CacheManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,15 @@ describe('CacheManager', () => {
const mockClass: TSClassInterface = {
name: 'TestClass',
filePath: testFilePath,
module: 'test',
decorators: [],
methods: [],
properties: [],

implements: [],
imports: [],
exports: [],
isAbstract: false,
isExported: true,
location: { filePath: testFilePath, line: 1, column: 0 },
};
const mockClasses = [new TSClass(mockClass)];

Expand Down Expand Up @@ -256,26 +258,30 @@ describe('CacheManager', () => {
new TSClass({
name: 'Class1',
filePath: 'file1.ts',
module: 'test1',
decorators: [],
methods: [],
properties: [],

implements: [],
imports: [],
exports: [],
isAbstract: false,
isExported: true,
location: { filePath: 'file1.ts', line: 1, column: 0 },
}),
];
const mockClasses2 = [
new TSClass({
name: 'Class2',
filePath: 'file2.ts',
module: 'test2',
decorators: [],
methods: [],
properties: [],

implements: [],
imports: [],
exports: [],
isAbstract: false,
isExported: true,
location: { filePath: 'file2.ts', line: 1, column: 0 },
}),
];

Expand Down
28 changes: 14 additions & 14 deletions test/ComprehensiveCoverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ describe('Comprehensive Coverage Tests', () => {

it('should add class to collection', () => {
const newCollection = new TSClasses();
const sampleClass = new TSClass(
'TestClass',
'/test/path.ts',
'test/module',
[],
[],
[],
[],
[],
[],
[],
false,
false
);
const sampleClass = new TSClass({
name: 'TestClass',
filePath: '/test/path.ts',
module: 'test/module',
implements: [],
decorators: [],
methods: [],
properties: [],
isAbstract: false,
isExported: false,
location: { filePath: '/test/path.ts', line: 1, column: 0 },
imports: [],
dependencies: [],
});
newCollection.add(sampleClass);
expect(newCollection.size()).toBe(1);
});
Expand Down
7 changes: 4 additions & 3 deletions test/PatternMatching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,14 @@ describe('Pattern Matching', () => {
fs.writeFileSync(path.join(level2Dir, 'Level2.ts'), 'export class Level2 {}', 'utf-8');

const classes = analyzer.analyze(tempDir).then((result) => {
// * should match only one level - looking for files in level1 directory
// resideInPackage matches the package and all sub-packages (like ArchUnit Java)
// Both Level1 (in level1) and Level2 (in level1/level2) should match
const singleLevel = result.resideInPackage('level1');

const classNames = singleLevel.getAll().map((c) => c.name);
// Should match level1 but not level2
// Should match both level1 and level2 (level2 is a subpackage of level1)
expect(classNames).toContain('Level1');
expect(classNames).not.toContain('Level2');
expect(classNames).toContain('Level2');

// Cleanup
fs.unlinkSync(path.join(level1Dir, 'Level1.ts'));
Expand Down
39 changes: 39 additions & 0 deletions test/analysis/SuggestionEngine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { SuggestionEngine } from '../../src/analysis/SuggestionEngine';
import { ArchitectureViolation, Severity } from '../../src/types';

describe('SuggestionEngine', () => {
let engine: SuggestionEngine;

beforeEach(() => {
engine = new SuggestionEngine();
});

it('should create suggestion engine', () => {
expect(engine).toBeDefined();
});

it('should generate fix for naming violations', () => {
const violation: ArchitectureViolation = {
message: "Class 'UserManager' should end with 'Service'",
filePath: '/src/UserManager.ts',
rule: 'Service naming convention',
severity: Severity.ERROR,
};

const fix = engine.generateFix(violation);
expect(fix).toBeDefined();
expect(fix?.description).toContain('Service');
});

it('should generate alternatives', () => {
const violation: ArchitectureViolation = {
message: "Class 'User' should end with 'Service'",
filePath: '/src/User.ts',
rule: "Services should end with 'Service'",
severity: Severity.ERROR,
};

const alternatives = engine.generateAlternatives(violation);
expect(alternatives).toContain('UserService');
});
});
58 changes: 58 additions & 0 deletions test/analysis/ViolationAnalyzer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ViolationAnalyzer } from '../../src/analysis/ViolationAnalyzer';
import { ArchitectureViolation, Severity } from '../../src/types';

describe('ViolationAnalyzer', () => {
let analyzer: ViolationAnalyzer;
let sampleViolations: ArchitectureViolation[];

beforeEach(() => {
sampleViolations = [
{
message: "Class 'UserService' should end with 'Repository'",
filePath: '/src/UserService.ts',
rule: 'Repository naming convention',
severity: Severity.ERROR,
},
{
message: "Class 'OrderService' should end with 'Repository'",
filePath: '/src/OrderService.ts',
rule: 'Repository naming convention',
severity: Severity.ERROR,
},
];
});

it('should create violation analyzer', () => {
analyzer = new ViolationAnalyzer(sampleViolations);
expect(analyzer).toBeDefined();
});

it('should enhance violations', () => {
analyzer = new ViolationAnalyzer(sampleViolations);
const enhanced = analyzer.enhance();

expect(enhanced).toHaveLength(2);
enhanced.forEach((v) => {
expect(v.id).toBeDefined();
expect(v.category).toBeDefined();
expect(v.impactScore).toBeDefined();
});
});

it('should group by root cause', () => {
analyzer = new ViolationAnalyzer(sampleViolations);
const groups = analyzer.groupByRootCause();

expect(groups).toBeDefined();
expect(groups.length).toBeGreaterThan(0);
});

it('should perform full analysis', () => {
analyzer = new ViolationAnalyzer(sampleViolations);
const analysis = analyzer.analyze();

expect(analysis.total).toBe(2);
expect(analysis.groups).toBeDefined();
expect(analysis.topPriority).toBeDefined();
});
});
47 changes: 47 additions & 0 deletions test/cli/ErrorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ErrorHandler, ErrorType } from '../../src/cli/ErrorHandler';

describe('ErrorHandler', () => {
let handler: ErrorHandler;

beforeEach(() => {
handler = new ErrorHandler(false); // Disable colors for testing
});

it('should create error handler', () => {
expect(handler).toBeDefined();
});

it('should parse configuration errors', () => {
const error = new Error('Cannot find module config');
const parsed = handler.parseError(error);

expect(parsed.type).toBe(ErrorType.CONFIGURATION);
expect(parsed.suggestions.length).toBeGreaterThan(0);
});

it('should parse file system errors', () => {
const error = new Error('ENOENT: no such file or directory');
const parsed = handler.parseError(error);

expect(parsed.type).toBe(ErrorType.FILE_SYSTEM);
expect(parsed.suggestions.length).toBeGreaterThan(0);
});

it('should format errors', () => {
const error = new Error('Test error');
const parsed = handler.parseError(error);
const formatted = handler.formatError(parsed);

expect(formatted).toBeDefined();
expect(typeof formatted).toBe('string');
expect(formatted).toContain('Error');
});

it('should format with suggestions', () => {
const error = new Error('Config not found');
const parsed = handler.parseError(error);
const formatted = handler.formatError(parsed);

expect(formatted).toContain('Suggestions');
});
});
Loading
Loading