feat: add dependency direction enforcement (v0.4.0)

Implement dependency direction detection to enforce Clean Architecture rules:
- Domain layer can only import from Domain and Shared
- Application layer can only import from Domain, Application, and Shared
- Infrastructure layer can import from all layers
- Shared layer can be imported by all layers

Added:
- IDependencyDirectionDetector interface in domain layer
- DependencyViolation value object with detailed suggestions and examples
- DependencyDirectionDetector implementation in infrastructure
- Integration with AnalyzeProject use case
- New DEPENDENCY_DIRECTION rule in constants
- 43 comprehensive tests covering all scenarios (100% passing)
- Good and bad examples in examples directory

Improvements:
- Optimized extractLayerFromImport method to reduce complexity
- Fixed indentation in DependencyGraph.ts
- Updated getExampleFix to avoid false positives in old detector

Test Results:
- All 261 tests passing
- Build successful
- Self-check: 0 architecture violations in src code
This commit is contained in:
imfozilbek
2025-11-24 18:31:41 +05:00
parent f46048172f
commit 3fecc98676
15 changed files with 1452 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector"
import { IFrameworkLeakDetector } from "../../domain/services/IFrameworkLeakDetector"
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
import { SourceFile } from "../../domain/entities/SourceFile"
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
import { ProjectPath } from "../../domain/value-objects/ProjectPath"
@@ -34,6 +35,7 @@ export interface AnalyzeProjectResponse {
namingViolations: NamingConventionViolation[]
frameworkLeakViolations: FrameworkLeakViolation[]
entityExposureViolations: EntityExposureViolation[]
dependencyDirectionViolations: DependencyDirectionViolation[]
metrics: ProjectMetrics
}
@@ -109,6 +111,17 @@ export interface EntityExposureViolation {
suggestion: string
}
export interface DependencyDirectionViolation {
rule: typeof RULES.DEPENDENCY_DIRECTION
fromLayer: string
toLayer: string
importPath: string
file: string
line?: number
message: string
suggestion: string
}
export interface ProjectMetrics {
totalFiles: number
totalFunctions: number
@@ -130,6 +143,7 @@ export class AnalyzeProject extends UseCase<
private readonly namingConventionDetector: INamingConventionDetector,
private readonly frameworkLeakDetector: IFrameworkLeakDetector,
private readonly entityExposureDetector: IEntityExposureDetector,
private readonly dependencyDirectionDetector: IDependencyDirectionDetector,
) {
super()
}
@@ -180,6 +194,7 @@ export class AnalyzeProject extends UseCase<
const namingViolations = this.detectNamingConventions(sourceFiles)
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
const entityExposureViolations = this.detectEntityExposures(sourceFiles)
const dependencyDirectionViolations = this.detectDependencyDirections(sourceFiles)
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
return ResponseDto.ok({
@@ -191,6 +206,7 @@ export class AnalyzeProject extends UseCase<
namingViolations,
frameworkLeakViolations,
entityExposureViolations,
dependencyDirectionViolations,
metrics,
})
} catch (error) {
@@ -409,6 +425,33 @@ export class AnalyzeProject extends UseCase<
return violations
}
private detectDependencyDirections(sourceFiles: SourceFile[]): DependencyDirectionViolation[] {
const violations: DependencyDirectionViolation[] = []
for (const file of sourceFiles) {
const directionViolations = this.dependencyDirectionDetector.detectViolations(
file.content,
file.path.relative,
file.layer,
)
for (const violation of directionViolations) {
violations.push({
rule: RULES.DEPENDENCY_DIRECTION,
fromLayer: violation.fromLayer,
toLayer: violation.toLayer,
importPath: violation.importPath,
file: file.path.relative,
line: violation.line,
message: violation.getMessage(),
suggestion: violation.getSuggestion(),
})
}
}
return violations
}
private calculateMetrics(
sourceFiles: SourceFile[],
totalFunctions: number,