mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
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:
@@ -94,7 +94,7 @@ export class DependencyGraph extends BaseEntity {
|
||||
totalDependencies: number
|
||||
avgDependencies: number
|
||||
maxDependencies: number
|
||||
} {
|
||||
} {
|
||||
const nodes = Array.from(this.nodes.values())
|
||||
const totalFiles = nodes.length
|
||||
const totalDependencies = nodes.reduce((sum, node) => sum + node.dependencies.length, 0)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DependencyViolation } from "../value-objects/DependencyViolation"
|
||||
|
||||
/**
|
||||
* Interface for detecting dependency direction violations in the codebase
|
||||
*
|
||||
* Dependency direction violations occur when a layer imports from a layer
|
||||
* that it should not depend on according to Clean Architecture principles:
|
||||
* - Domain should not import from Application or Infrastructure
|
||||
* - Application should not import from Infrastructure
|
||||
* - Infrastructure can import from Application and Domain
|
||||
* - Shared can be imported by all layers
|
||||
*/
|
||||
export interface IDependencyDirectionDetector {
|
||||
/**
|
||||
* Detects dependency direction violations in the given code
|
||||
*
|
||||
* Analyzes import statements to identify violations of dependency rules
|
||||
* between architectural layers.
|
||||
*
|
||||
* @param code - Source code to analyze
|
||||
* @param filePath - Path to the file being analyzed
|
||||
* @param layer - The architectural layer of the file (domain, application, infrastructure, shared)
|
||||
* @returns Array of detected dependency direction violations
|
||||
*/
|
||||
detectViolations(
|
||||
code: string,
|
||||
filePath: string,
|
||||
layer: string | undefined,
|
||||
): DependencyViolation[]
|
||||
|
||||
/**
|
||||
* Checks if an import violates dependency direction rules
|
||||
*
|
||||
* @param fromLayer - The layer that is importing
|
||||
* @param toLayer - The layer being imported
|
||||
* @returns True if the import violates dependency rules
|
||||
*/
|
||||
isViolation(fromLayer: string, toLayer: string): boolean
|
||||
|
||||
/**
|
||||
* Extracts the layer from an import path
|
||||
*
|
||||
* @param importPath - The import path to analyze
|
||||
* @returns The layer name if detected, undefined otherwise
|
||||
*/
|
||||
extractLayerFromImport(importPath: string): string | undefined
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
|
||||
interface DependencyViolationProps {
|
||||
readonly fromLayer: string
|
||||
readonly toLayer: string
|
||||
readonly importPath: string
|
||||
readonly filePath: string
|
||||
readonly line?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a dependency direction violation in the codebase
|
||||
*
|
||||
* Dependency direction violations occur when a layer imports from a layer
|
||||
* that it should not depend on according to Clean Architecture principles:
|
||||
* - Domain → should not import from Application or Infrastructure
|
||||
* - Application → should not import from Infrastructure
|
||||
* - Infrastructure → can import from Application and Domain (allowed)
|
||||
* - Shared → can be imported by all layers (allowed)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Bad: Domain importing from Application
|
||||
* const violation = DependencyViolation.create(
|
||||
* 'domain',
|
||||
* 'application',
|
||||
* '../../application/dtos/UserDto',
|
||||
* 'src/domain/entities/User.ts',
|
||||
* 5
|
||||
* )
|
||||
*
|
||||
* console.log(violation.getMessage())
|
||||
* // "Domain layer should not import from Application layer"
|
||||
* ```
|
||||
*/
|
||||
export class DependencyViolation extends ValueObject<DependencyViolationProps> {
|
||||
private constructor(props: DependencyViolationProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(
|
||||
fromLayer: string,
|
||||
toLayer: string,
|
||||
importPath: string,
|
||||
filePath: string,
|
||||
line?: number,
|
||||
): DependencyViolation {
|
||||
return new DependencyViolation({
|
||||
fromLayer,
|
||||
toLayer,
|
||||
importPath,
|
||||
filePath,
|
||||
line,
|
||||
})
|
||||
}
|
||||
|
||||
public get fromLayer(): string {
|
||||
return this.props.fromLayer
|
||||
}
|
||||
|
||||
public get toLayer(): string {
|
||||
return this.props.toLayer
|
||||
}
|
||||
|
||||
public get importPath(): string {
|
||||
return this.props.importPath
|
||||
}
|
||||
|
||||
public get filePath(): string {
|
||||
return this.props.filePath
|
||||
}
|
||||
|
||||
public get line(): number | undefined {
|
||||
return this.props.line
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
return `${this.capitalizeFirst(this.props.fromLayer)} layer should not import from ${this.capitalizeFirst(this.props.toLayer)} layer`
|
||||
}
|
||||
|
||||
public getSuggestion(): string {
|
||||
const suggestions: string[] = []
|
||||
|
||||
if (this.props.fromLayer === "domain") {
|
||||
suggestions.push(
|
||||
"Domain layer should be independent and not depend on other layers",
|
||||
"Move the imported code to the domain layer if it contains business logic",
|
||||
"Use dependency inversion: define an interface in domain and implement it in infrastructure",
|
||||
)
|
||||
} else if (this.props.fromLayer === "application") {
|
||||
suggestions.push(
|
||||
"Application layer should not depend on infrastructure",
|
||||
"Define an interface (Port) in application layer",
|
||||
"Implement the interface (Adapter) in infrastructure layer",
|
||||
"Use dependency injection to provide the implementation",
|
||||
)
|
||||
}
|
||||
|
||||
return suggestions.join("\n")
|
||||
}
|
||||
|
||||
public getExampleFix(): string {
|
||||
if (this.props.fromLayer === "domain" && this.props.toLayer === "infrastructure") {
|
||||
return `
|
||||
// ❌ Bad: Domain depends on Infrastructure (PrismaClient)
|
||||
// domain/services/UserService.ts
|
||||
class UserService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
}
|
||||
|
||||
// ✅ Good: Domain defines interface, Infrastructure implements
|
||||
// domain/repositories/IUserRepository.ts
|
||||
interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
}
|
||||
|
||||
// domain/services/UserService.ts
|
||||
class UserService {
|
||||
constructor(private userRepo: IUserRepository) {}
|
||||
}
|
||||
|
||||
// infrastructure/repositories/PrismaUserRepository.ts
|
||||
class PrismaUserRepository implements IUserRepository {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
async findById(id: UserId): Promise<User | null> { }
|
||||
async save(user: User): Promise<void> { }
|
||||
}`
|
||||
}
|
||||
|
||||
if (this.props.fromLayer === "application" && this.props.toLayer === "infrastructure") {
|
||||
return `
|
||||
// ❌ Bad: Application depends on Infrastructure (SmtpEmailService)
|
||||
// application/use-cases/SendEmail.ts
|
||||
class SendWelcomeEmail {
|
||||
constructor(private emailService: SmtpEmailService) {}
|
||||
}
|
||||
|
||||
// ✅ Good: Application defines Port, Infrastructure implements Adapter
|
||||
// application/ports/IEmailService.ts
|
||||
interface IEmailService {
|
||||
send(to: string, subject: string, body: string): Promise<void>
|
||||
}
|
||||
|
||||
// application/use-cases/SendEmail.ts
|
||||
class SendWelcomeEmail {
|
||||
constructor(private emailService: IEmailService) {}
|
||||
}
|
||||
|
||||
// infrastructure/adapters/SmtpEmailService.ts
|
||||
class SmtpEmailService implements IEmailService {
|
||||
async send(to: string, subject: string, body: string): Promise<void> { }
|
||||
}`
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private capitalizeFirst(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user