From 3fecc98676244abed4e7e3d8b241911cb0d0e988 Mon Sep 17 00:00:00 2001 From: imfozilbek Date: Mon, 24 Nov 2025 18:31:41 +0500 Subject: [PATCH] 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 --- packages/guardian/ROADMAP.md | 27 +- .../BadApplicationLayer.ts | 117 ++++ .../dependency-direction/BadDomainLayer.ts | 90 +++ .../GoodApplicationLayer.ts | 91 ++++ .../dependency-direction/GoodDomainLayer.ts | 56 ++ .../GoodInfrastructureLayer.ts | 128 +++++ packages/guardian/package.json | 2 +- packages/guardian/src/api.ts | 6 + .../application/use-cases/AnalyzeProject.ts | 43 ++ .../src/domain/entities/DependencyGraph.ts | 2 +- .../services/IDependencyDirectionDetector.ts | 47 ++ .../value-objects/DependencyViolation.ts | 162 ++++++ .../analyzers/DependencyDirectionDetector.ts | 183 +++++++ .../guardian/src/shared/constants/rules.ts | 1 + .../tests/DependencyDirectionDetector.test.ts | 511 ++++++++++++++++++ 15 files changed, 1452 insertions(+), 14 deletions(-) create mode 100644 packages/guardian/examples/bad-architecture/dependency-direction/BadApplicationLayer.ts create mode 100644 packages/guardian/examples/bad-architecture/dependency-direction/BadDomainLayer.ts create mode 100644 packages/guardian/examples/good-architecture/dependency-direction/GoodApplicationLayer.ts create mode 100644 packages/guardian/examples/good-architecture/dependency-direction/GoodDomainLayer.ts create mode 100644 packages/guardian/examples/good-architecture/dependency-direction/GoodInfrastructureLayer.ts create mode 100644 packages/guardian/src/domain/services/IDependencyDirectionDetector.ts create mode 100644 packages/guardian/src/domain/value-objects/DependencyViolation.ts create mode 100644 packages/guardian/src/infrastructure/analyzers/DependencyDirectionDetector.ts create mode 100644 packages/guardian/tests/DependencyDirectionDetector.test.ts diff --git a/packages/guardian/ROADMAP.md b/packages/guardian/ROADMAP.md index b1b239a..3525c9c 100644 --- a/packages/guardian/ROADMAP.md +++ b/packages/guardian/ROADMAP.md @@ -2,7 +2,7 @@ This document outlines the current features and future plans for @puaros/guardian. -## Current Version: 0.3.0 βœ… RELEASED +## Current Version: 0.4.0 βœ… RELEASED **Released:** 2025-11-24 @@ -71,10 +71,9 @@ async getUser(id: string): Promise { --- -## Future Roadmap +## Version 0.4.0 - Dependency Direction Enforcement 🎯 βœ… RELEASED -### Version 0.4.0 - Dependency Direction Enforcement 🎯 -**Target:** Q1 2026 +**Released:** 2025-11-24 **Priority:** HIGH Enforce correct dependency direction between architectural layers: @@ -103,16 +102,20 @@ import { User } from '../../domain/entities/User' // OK - βœ… Infrastructure β†’ Application, Domain (Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΎ) - βœ… Shared β†’ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π²Π΅Π·Π΄Π΅ -**Planned Features:** -- Detect domain importing from application -- Detect domain importing from infrastructure -- Detect application importing from infrastructure -- Visualize dependency graph -- Suggest refactoring to fix violations -- Support for custom layer definitions +**Implemented Features:** +- βœ… Detect domain importing from application +- βœ… Detect domain importing from infrastructure +- βœ… Detect application importing from infrastructure +- βœ… Detect violations in all import formats (ES6, require) +- βœ… Provide detailed error messages with suggestions +- βœ… Show example fixes for each violation type +- βœ… 43 tests covering all dependency scenarios +- βœ… Good and bad examples in examples directory --- +## Future Roadmap + ### Version 0.5.0 - Repository Pattern Validation πŸ“š **Target:** Q1 2026 **Priority:** HIGH @@ -1748,4 +1751,4 @@ Until we reach 1.0.0, minor version bumps (0.x.0) may include breaking changes a --- **Last Updated:** 2025-11-24 -**Current Version:** 0.3.0 +**Current Version:** 0.4.0 diff --git a/packages/guardian/examples/bad-architecture/dependency-direction/BadApplicationLayer.ts b/packages/guardian/examples/bad-architecture/dependency-direction/BadApplicationLayer.ts new file mode 100644 index 0000000..04fe4a2 --- /dev/null +++ b/packages/guardian/examples/bad-architecture/dependency-direction/BadApplicationLayer.ts @@ -0,0 +1,117 @@ +/** + * ❌ BAD: Application layer with incorrect dependencies + * + * Application importing from Infrastructure layer + * This violates Clean Architecture dependency rules! + */ + +import { User } from "../../good-architecture/domain/entities/User" +import { Email } from "../../good-architecture/domain/value-objects/Email" +import { UserId } from "../../good-architecture/domain/value-objects/UserId" + +/** + * ❌ VIOLATION: Application importing from Infrastructure layer + */ +import { PrismaClient } from "@prisma/client" + +export class CreateUser { + /** + * ❌ VIOLATION: Application use case depending on concrete infrastructure (Prisma) + */ + constructor(private prisma: PrismaClient) {} + + async execute(email: string): Promise { + const userId = UserId.generate() + const emailVO = Email.create(email).value + + /** + * ❌ VIOLATION: Application logic directly accessing database + */ + await this.prisma.user.create({ + data: { + id: userId.getValue(), + email: emailVO.getValue(), + }, + }) + + return new User(userId, emailVO) + } +} + +/** + * ❌ VIOLATION: Application importing concrete email service from infrastructure + */ +import { SmtpEmailService } from "../../good-architecture/infrastructure/adapters/SmtpEmailService" + +export class SendWelcomeEmail { + /** + * ❌ VIOLATION: Application depending on concrete infrastructure implementation + * Should depend on IEmailService interface instead + */ + constructor( + private prisma: PrismaClient, + private emailService: SmtpEmailService, + ) {} + + async execute(userId: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }) + + if (!user) { + throw new Error("User not found") + } + + await this.emailService.sendWelcomeEmail(user.email) + } +} + +/** + * ❌ VIOLATION: Application importing from infrastructure controller + */ +import { UserController } from "../../good-architecture/infrastructure/controllers/UserController" + +export class ValidateUser { + /** + * ❌ VIOLATION: Application use case depending on infrastructure controller + * The dependency direction is completely wrong! + */ + constructor(private userController: UserController) {} + + async execute(userId: string): Promise { + return true + } +} + +/** + * ❌ VIOLATION: Application importing HTTP framework + */ +import express from "express" + +export class ProcessUserRequest { + /** + * ❌ VIOLATION: Application layer knows about HTTP/Express + * HTTP concerns should be in infrastructure layer + */ + async execute(req: express.Request): Promise { + const email = req.body.email + console.log(`Processing user: ${email}`) + } +} + +/** + * ❌ VIOLATION: Application importing infrastructure repository implementation + */ +import { InMemoryUserRepository } from "../../good-architecture/infrastructure/repositories/InMemoryUserRepository" + +export class GetUser { + /** + * ❌ VIOLATION: Application depending on concrete repository implementation + * Should depend on IUserRepository interface from domain + */ + constructor(private userRepo: InMemoryUserRepository) {} + + async execute(userId: string): Promise { + return await this.userRepo.findById(UserId.from(userId)) + } +} diff --git a/packages/guardian/examples/bad-architecture/dependency-direction/BadDomainLayer.ts b/packages/guardian/examples/bad-architecture/dependency-direction/BadDomainLayer.ts new file mode 100644 index 0000000..313a377 --- /dev/null +++ b/packages/guardian/examples/bad-architecture/dependency-direction/BadDomainLayer.ts @@ -0,0 +1,90 @@ +/** + * ❌ BAD: Domain layer with incorrect dependencies + * + * Domain importing from Application and Infrastructure layers + * This violates Clean Architecture dependency rules! + */ + +import { Email } from "../../good-architecture/domain/value-objects/Email" +import { UserId } from "../../good-architecture/domain/value-objects/UserId" + +/** + * ❌ VIOLATION: Domain importing from Application layer + */ +import { UserResponseDto } from "../../good-architecture/application/dtos/UserResponseDto" + +export class User { + private readonly id: UserId + private email: Email + + constructor(id: UserId, email: Email) { + this.id = id + this.email = email + } + + /** + * ❌ VIOLATION: Domain entity returning DTO from application layer + */ + toDto(): UserResponseDto { + return { + id: this.id.getValue(), + email: this.email.getValue(), + createdAt: new Date().toISOString(), + } + } +} + +/** + * ❌ VIOLATION: Domain importing from Infrastructure layer + */ +import { PrismaClient } from "@prisma/client" + +export class UserService { + /** + * ❌ VIOLATION: Domain service depending on concrete infrastructure implementation + */ + constructor(private prisma: PrismaClient) {} + + async createUser(email: string): Promise { + const userId = UserId.generate() + const emailVO = Email.create(email).value + + /** + * ❌ VIOLATION: Domain logic directly accessing database + */ + await this.prisma.user.create({ + data: { + id: userId.getValue(), + email: emailVO.getValue(), + }, + }) + + return new User(userId, emailVO) + } +} + +/** + * ❌ VIOLATION: Domain importing email service from infrastructure + */ +import { SmtpEmailService } from "../../good-architecture/infrastructure/adapters/SmtpEmailService" + +export class UserRegistration { + /** + * ❌ VIOLATION: Domain depending on infrastructure email service + */ + constructor( + private userService: UserService, + private emailService: SmtpEmailService, + ) {} + + async register(email: string): Promise { + const user = await this.userService.createUser(email) + + /** + * ❌ VIOLATION: Domain calling infrastructure service directly + */ + await this.emailService.sendWelcomeEmail(email) + + return user + } +} diff --git a/packages/guardian/examples/good-architecture/dependency-direction/GoodApplicationLayer.ts b/packages/guardian/examples/good-architecture/dependency-direction/GoodApplicationLayer.ts new file mode 100644 index 0000000..e6b7d1e --- /dev/null +++ b/packages/guardian/examples/good-architecture/dependency-direction/GoodApplicationLayer.ts @@ -0,0 +1,91 @@ +/** + * βœ… GOOD: Application layer with correct dependencies + * + * Application should only import from: + * - Domain layer + * - Other application files + * - Shared utilities + * + * Application should NOT import from: + * - Infrastructure layer + */ + +import { User } from "../domain/entities/User" +import { Email } from "../domain/value-objects/Email" +import { UserId } from "../domain/value-objects/UserId" +import { IUserRepository } from "../domain/repositories/IUserRepository" +import { Result } from "../../../src/shared/types/Result" + +/** + * βœ… Use case depends on domain interfaces (IUserRepository) + * NOT on infrastructure implementations + */ +export class CreateUser { + constructor(private readonly userRepo: IUserRepository) {} + + async execute(request: CreateUserRequest): Promise> { + const emailResult = Email.create(request.email) + if (emailResult.isFailure) { + return Result.fail(emailResult.error) + } + + const userId = UserId.generate() + const user = new User(userId, emailResult.value) + + await this.userRepo.save(user) + + return Result.ok(UserMapper.toDto(user)) + } +} + +/** + * βœ… DTO in application layer + */ +export interface CreateUserRequest { + email: string + name: string +} + +export interface UserResponseDto { + id: string + email: string + createdAt: string +} + +/** + * βœ… Mapper in application layer converting domain to DTO + */ +export class UserMapper { + static toDto(user: User): UserResponseDto { + return { + id: user.getId().getValue(), + email: user.getEmail().getValue(), + createdAt: user.getCreatedAt().toISOString(), + } + } +} + +/** + * βœ… Application defines Port (interface) for email service + * Infrastructure will provide the Adapter (implementation) + */ +export interface IEmailService { + sendWelcomeEmail(email: string): Promise +} + +export class SendWelcomeEmail { + constructor( + private readonly userRepo: IUserRepository, + private readonly emailService: IEmailService, + ) {} + + async execute(userId: string): Promise> { + const user = await this.userRepo.findById(UserId.from(userId)) + if (!user) { + return Result.fail("User not found") + } + + await this.emailService.sendWelcomeEmail(user.getEmail().getValue()) + return Result.ok() + } +} diff --git a/packages/guardian/examples/good-architecture/dependency-direction/GoodDomainLayer.ts b/packages/guardian/examples/good-architecture/dependency-direction/GoodDomainLayer.ts new file mode 100644 index 0000000..acf3aa9 --- /dev/null +++ b/packages/guardian/examples/good-architecture/dependency-direction/GoodDomainLayer.ts @@ -0,0 +1,56 @@ +/** + * βœ… GOOD: Domain layer with correct dependencies + * + * Domain should only import from: + * - Other domain files + * - Shared utilities + * + * Domain should NOT import from: + * - Application layer + * - Infrastructure layer + */ + +import { Email } from "../domain/value-objects/Email" +import { UserId } from "../domain/value-objects/UserId" +import { Result } from "../../../src/shared/types/Result" + +/** + * βœ… Domain entity using only domain value objects and shared types + */ +export class User { + private readonly id: UserId + private email: Email + private readonly createdAt: Date + + constructor(id: UserId, email: Email, createdAt: Date = new Date()) { + this.id = id + this.email = email + this.createdAt = createdAt + } + + public getId(): UserId { + return this.id + } + + public getEmail(): Email { + return this.email + } + + public changeEmail(newEmail: Email): Result { + if (this.email.equals(newEmail)) { + return Result.fail("Email is the same") + } + + this.email = newEmail + return Result.ok() + } +} + +/** + * βœ… Domain repository interface (not importing from infrastructure) + */ +export interface IUserRepository { + findById(id: UserId): Promise + save(user: User): Promise + delete(id: UserId): Promise +} diff --git a/packages/guardian/examples/good-architecture/dependency-direction/GoodInfrastructureLayer.ts b/packages/guardian/examples/good-architecture/dependency-direction/GoodInfrastructureLayer.ts new file mode 100644 index 0000000..c7dfe49 --- /dev/null +++ b/packages/guardian/examples/good-architecture/dependency-direction/GoodInfrastructureLayer.ts @@ -0,0 +1,128 @@ +/** + * βœ… GOOD: Infrastructure layer with correct dependencies + * + * Infrastructure CAN import from: + * - Domain layer + * - Application layer + * - Other infrastructure files + * - Shared utilities + * - External libraries (ORM, frameworks, etc.) + */ + +import { User } from "../domain/entities/User" +import { UserId } from "../domain/value-objects/UserId" +import { Email } from "../domain/value-objects/Email" +import { IUserRepository } from "../domain/repositories/IUserRepository" +import { CreateUser } from "../application/use-cases/CreateUser" +import { UserResponseDto } from "../application/dtos/UserResponseDto" +import { IEmailService } from "../application/ports/IEmailService" + +/** + * βœ… Infrastructure implements domain interface + */ +export class InMemoryUserRepository implements IUserRepository { + private users: Map = new Map() + + async findById(id: UserId): Promise { + return this.users.get(id.getValue()) ?? null + } + + async save(user: User): Promise { + this.users.set(user.getId().getValue(), user) + } + + async delete(id: UserId): Promise { + this.users.delete(id.getValue()) + } +} + +/** + * βœ… Infrastructure provides Adapter implementing application Port + */ +export class SmtpEmailService implements IEmailService { + constructor( + private readonly host: string, + private readonly port: number, + ) {} + + async sendWelcomeEmail(email: string): Promise { + console.log(`Sending welcome email to ${email} via SMTP`) + } +} + +/** + * βœ… Controller uses application use cases and DTOs + */ +export class UserController { + constructor(private readonly createUser: CreateUser) {} + + async create(request: { email: string; name: string }): Promise { + const result = await this.createUser.execute(request) + + if (result.isFailure) { + throw new Error(result.error) + } + + return result.value + } +} + +/** + * βœ… Infrastructure can use external frameworks + */ +import express from "express" + +export class ExpressServer { + private app = express() + + constructor(private readonly userController: UserController) { + this.setupRoutes() + } + + private setupRoutes(): void { + this.app.post("/users", async (req, res) => { + const user = await this.userController.create(req.body) + res.json(user) + }) + } +} + +/** + * βœ… Infrastructure can use ORM + */ +import { PrismaClient } from "@prisma/client" + +export class PrismaUserRepository implements IUserRepository { + constructor(private readonly prisma: PrismaClient) {} + + async findById(id: UserId): Promise { + const userData = await this.prisma.user.findUnique({ + where: { id: id.getValue() }, + }) + + if (!userData) { + return null + } + + return new User(UserId.from(userData.id), Email.create(userData.email).value) + } + + async save(user: User): Promise { + await this.prisma.user.upsert({ + where: { id: user.getId().getValue() }, + create: { + id: user.getId().getValue(), + email: user.getEmail().getValue(), + }, + update: { + email: user.getEmail().getValue(), + }, + }) + } + + async delete(id: UserId): Promise { + await this.prisma.user.delete({ + where: { id: id.getValue() }, + }) + } +} diff --git a/packages/guardian/package.json b/packages/guardian/package.json index 41a1779..6fd1d4d 100644 --- a/packages/guardian/package.json +++ b/packages/guardian/package.json @@ -1,6 +1,6 @@ { "name": "@samiyev/guardian", - "version": "0.3.0", + "version": "0.4.0", "description": "Code quality guardian for vibe coders and enterprise teams - catch hardcodes, architecture violations, and circular deps. Enforce Clean Architecture at scale. Works with Claude, GPT, Copilot.", "keywords": [ "puaros", diff --git a/packages/guardian/src/api.ts b/packages/guardian/src/api.ts index c756ea4..d6ded8e 100644 --- a/packages/guardian/src/api.ts +++ b/packages/guardian/src/api.ts @@ -9,12 +9,14 @@ 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 { FileScanner } from "./infrastructure/scanners/FileScanner" import { CodeParser } from "./infrastructure/parsers/CodeParser" import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector" import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConventionDetector" import { FrameworkLeakDetector } from "./infrastructure/analyzers/FrameworkLeakDetector" import { EntityExposureDetector } from "./infrastructure/analyzers/EntityExposureDetector" +import { DependencyDirectionDetector } from "./infrastructure/analyzers/DependencyDirectionDetector" import { ERROR_MESSAGES } from "./shared/constants" /** @@ -69,6 +71,8 @@ export async function analyzeProject( const namingConventionDetector: INamingConventionDetector = new NamingConventionDetector() const frameworkLeakDetector: IFrameworkLeakDetector = new FrameworkLeakDetector() const entityExposureDetector: IEntityExposureDetector = new EntityExposureDetector() + const dependencyDirectionDetector: IDependencyDirectionDetector = + new DependencyDirectionDetector() const useCase = new AnalyzeProject( fileScanner, codeParser, @@ -76,6 +80,7 @@ export async function analyzeProject( namingConventionDetector, frameworkLeakDetector, entityExposureDetector, + dependencyDirectionDetector, ) const result = await useCase.execute(options) @@ -96,5 +101,6 @@ export type { NamingConventionViolation, FrameworkLeakViolation, EntityExposureViolation, + DependencyDirectionViolation, ProjectMetrics, } from "./application/use-cases/AnalyzeProject" diff --git a/packages/guardian/src/application/use-cases/AnalyzeProject.ts b/packages/guardian/src/application/use-cases/AnalyzeProject.ts index ee89d3d..9c55fe2 100644 --- a/packages/guardian/src/application/use-cases/AnalyzeProject.ts +++ b/packages/guardian/src/application/use-cases/AnalyzeProject.ts @@ -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, diff --git a/packages/guardian/src/domain/entities/DependencyGraph.ts b/packages/guardian/src/domain/entities/DependencyGraph.ts index 830e604..bb2bd97 100644 --- a/packages/guardian/src/domain/entities/DependencyGraph.ts +++ b/packages/guardian/src/domain/entities/DependencyGraph.ts @@ -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) diff --git a/packages/guardian/src/domain/services/IDependencyDirectionDetector.ts b/packages/guardian/src/domain/services/IDependencyDirectionDetector.ts new file mode 100644 index 0000000..69679da --- /dev/null +++ b/packages/guardian/src/domain/services/IDependencyDirectionDetector.ts @@ -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 +} diff --git a/packages/guardian/src/domain/value-objects/DependencyViolation.ts b/packages/guardian/src/domain/value-objects/DependencyViolation.ts new file mode 100644 index 0000000..5f60056 --- /dev/null +++ b/packages/guardian/src/domain/value-objects/DependencyViolation.ts @@ -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 { + 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 + save(user: User): Promise +} + +// 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 { } + async save(user: User): Promise { } +}` + } + + 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 +} + +// 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 { } +}` + } + + return "" + } + + private capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) + } +} diff --git a/packages/guardian/src/infrastructure/analyzers/DependencyDirectionDetector.ts b/packages/guardian/src/infrastructure/analyzers/DependencyDirectionDetector.ts new file mode 100644 index 0000000..aa0e138 --- /dev/null +++ b/packages/guardian/src/infrastructure/analyzers/DependencyDirectionDetector.ts @@ -0,0 +1,183 @@ +import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector" +import { DependencyViolation } from "../../domain/value-objects/DependencyViolation" +import { LAYERS } from "../../shared/constants/rules" + +/** + * Detects dependency direction violations between architectural layers + * + * This detector enforces Clean Architecture dependency rules: + * - 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 + * const detector = new DependencyDirectionDetector() + * + * // Detect violations in domain file + * const code = ` + * import { PrismaClient } from '@prisma/client' + * import { UserDto } from '../application/dtos/UserDto' + * ` + * const violations = detector.detectViolations(code, 'src/domain/entities/User.ts', 'domain') + * + * // violations will contain 1 violation for domain importing from application + * console.log(violations.length) // 1 + * console.log(violations[0].getMessage()) + * // "Domain layer should not import from Application layer" + * ``` + */ +export class DependencyDirectionDetector implements IDependencyDirectionDetector { + private readonly dependencyRules: Map> + + constructor() { + this.dependencyRules = new Map([ + [LAYERS.DOMAIN, new Set([LAYERS.DOMAIN, LAYERS.SHARED])], + [LAYERS.APPLICATION, new Set([LAYERS.DOMAIN, LAYERS.APPLICATION, LAYERS.SHARED])], + [ + LAYERS.INFRASTRUCTURE, + new Set([LAYERS.DOMAIN, LAYERS.APPLICATION, LAYERS.INFRASTRUCTURE, LAYERS.SHARED]), + ], + [ + LAYERS.SHARED, + new Set([LAYERS.DOMAIN, LAYERS.APPLICATION, LAYERS.INFRASTRUCTURE, LAYERS.SHARED]), + ], + ]) + } + + /** + * 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 + */ + public detectViolations( + code: string, + filePath: string, + layer: string | undefined, + ): DependencyViolation[] { + if (!layer || layer === LAYERS.SHARED) { + return [] + } + + const violations: DependencyViolation[] = [] + const lines = code.split("\n") + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const lineNumber = i + 1 + + const imports = this.extractImports(line) + for (const importPath of imports) { + const targetLayer = this.extractLayerFromImport(importPath) + + if (targetLayer && this.isViolation(layer, targetLayer)) { + violations.push( + DependencyViolation.create( + layer, + targetLayer, + importPath, + filePath, + lineNumber, + ), + ) + } + } + } + + return violations + } + + /** + * 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 + */ + public isViolation(fromLayer: string, toLayer: string): boolean { + const allowedDependencies = this.dependencyRules.get(fromLayer) + + if (!allowedDependencies) { + return false + } + + return !allowedDependencies.has(toLayer) + } + + /** + * Extracts the layer from an import path + * + * @param importPath - The import path to analyze + * @returns The layer name if detected, undefined otherwise + */ + public extractLayerFromImport(importPath: string): string | undefined { + const normalizedPath = importPath.replace(/['"]/g, "").toLowerCase() + + const layerPatterns: Array<[string, string]> = [ + [LAYERS.DOMAIN, "/domain/"], + [LAYERS.APPLICATION, "/application/"], + [LAYERS.INFRASTRUCTURE, "/infrastructure/"], + [LAYERS.SHARED, "/shared/"], + ] + + for (const [layer, pattern] of layerPatterns) { + if (this.containsLayerPattern(normalizedPath, pattern)) { + return layer + } + } + + return undefined + } + + /** + * Checks if the normalized path contains the layer pattern + */ + private containsLayerPattern(normalizedPath: string, pattern: string): boolean { + return ( + normalizedPath.includes(pattern) || + normalizedPath.includes(`.${pattern}`) || + normalizedPath.includes(`..${pattern}`) || + normalizedPath.includes(`...${pattern}`) + ) + } + + /** + * Extracts import paths from a line of code + * + * Handles various import statement formats: + * - import { X } from 'path' + * - import X from 'path' + * - import * as X from 'path' + * - const X = require('path') + * + * @param line - A line of code to analyze + * @returns Array of import paths found in the line + */ + private extractImports(line: string): string[] { + const imports: string[] = [] + + const esImportRegex = + /import\s+(?:{[^}]*}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)\s+from\s+['"]([^'"]+)['"]/g + let match = esImportRegex.exec(line) + while (match) { + imports.push(match[1]) + match = esImportRegex.exec(line) + } + + const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g + match = requireRegex.exec(line) + while (match) { + imports.push(match[1]) + match = requireRegex.exec(line) + } + + return imports + } +} diff --git a/packages/guardian/src/shared/constants/rules.ts b/packages/guardian/src/shared/constants/rules.ts index 086e588..8656383 100644 --- a/packages/guardian/src/shared/constants/rules.ts +++ b/packages/guardian/src/shared/constants/rules.ts @@ -8,6 +8,7 @@ export const RULES = { NAMING_CONVENTION: "naming-convention", FRAMEWORK_LEAK: "framework-leak", ENTITY_EXPOSURE: "entity-exposure", + DEPENDENCY_DIRECTION: "dependency-direction", } as const /** diff --git a/packages/guardian/tests/DependencyDirectionDetector.test.ts b/packages/guardian/tests/DependencyDirectionDetector.test.ts new file mode 100644 index 0000000..8f3a423 --- /dev/null +++ b/packages/guardian/tests/DependencyDirectionDetector.test.ts @@ -0,0 +1,511 @@ +import { describe, it, expect } from "vitest" +import { DependencyDirectionDetector } from "../src/infrastructure/analyzers/DependencyDirectionDetector" +import { LAYERS } from "../src/shared/constants/rules" + +describe("DependencyDirectionDetector", () => { + const detector = new DependencyDirectionDetector() + + describe("extractLayerFromImport", () => { + it("should extract domain layer from import path", () => { + expect(detector.extractLayerFromImport("../domain/entities/User")).toBe(LAYERS.DOMAIN) + expect(detector.extractLayerFromImport("../../domain/value-objects/Email")).toBe( + LAYERS.DOMAIN, + ) + expect(detector.extractLayerFromImport("../../../domain/services/UserService")).toBe( + LAYERS.DOMAIN, + ) + }) + + it("should extract application layer from import path", () => { + expect(detector.extractLayerFromImport("../application/use-cases/CreateUser")).toBe( + LAYERS.APPLICATION, + ) + expect(detector.extractLayerFromImport("../../application/dtos/UserDto")).toBe( + LAYERS.APPLICATION, + ) + expect(detector.extractLayerFromImport("../../../application/mappers/UserMapper")).toBe( + LAYERS.APPLICATION, + ) + }) + + it("should extract infrastructure layer from import path", () => { + expect( + detector.extractLayerFromImport("../infrastructure/controllers/UserController"), + ).toBe(LAYERS.INFRASTRUCTURE) + expect( + detector.extractLayerFromImport("../../infrastructure/repositories/UserRepository"), + ).toBe(LAYERS.INFRASTRUCTURE) + }) + + it("should extract shared layer from import path", () => { + expect(detector.extractLayerFromImport("../shared/types/Result")).toBe(LAYERS.SHARED) + expect(detector.extractLayerFromImport("../../shared/constants/rules")).toBe( + LAYERS.SHARED, + ) + }) + + it("should return undefined for non-layer imports", () => { + expect(detector.extractLayerFromImport("express")).toBeUndefined() + expect(detector.extractLayerFromImport("../utils/helper")).toBeUndefined() + expect(detector.extractLayerFromImport("../../lib/logger")).toBeUndefined() + }) + }) + + describe("isViolation", () => { + it("should allow domain to import from domain", () => { + expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.DOMAIN)).toBe(false) + }) + + it("should allow domain to import from shared", () => { + expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.SHARED)).toBe(false) + }) + + it("should NOT allow domain to import from application", () => { + expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.APPLICATION)).toBe(true) + }) + + it("should NOT allow domain to import from infrastructure", () => { + expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.INFRASTRUCTURE)).toBe(true) + }) + + it("should allow application to import from domain", () => { + expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.DOMAIN)).toBe(false) + }) + + it("should allow application to import from application", () => { + expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.APPLICATION)).toBe(false) + }) + + it("should allow application to import from shared", () => { + expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.SHARED)).toBe(false) + }) + + it("should NOT allow application to import from infrastructure", () => { + expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.INFRASTRUCTURE)).toBe(true) + }) + + it("should allow infrastructure to import from domain", () => { + expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.DOMAIN)).toBe(false) + }) + + it("should allow infrastructure to import from application", () => { + expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.APPLICATION)).toBe(false) + }) + + it("should allow infrastructure to import from infrastructure", () => { + expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.INFRASTRUCTURE)).toBe(false) + }) + + it("should allow infrastructure to import from shared", () => { + expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.SHARED)).toBe(false) + }) + + it("should allow shared to import from any layer", () => { + expect(detector.isViolation(LAYERS.SHARED, LAYERS.DOMAIN)).toBe(false) + expect(detector.isViolation(LAYERS.SHARED, LAYERS.APPLICATION)).toBe(false) + expect(detector.isViolation(LAYERS.SHARED, LAYERS.INFRASTRUCTURE)).toBe(false) + expect(detector.isViolation(LAYERS.SHARED, LAYERS.SHARED)).toBe(false) + }) + }) + + describe("detectViolations", () => { + describe("Domain layer violations", () => { + it("should detect domain importing from application", () => { + const code = ` +import { UserDto } from '../../application/dtos/UserDto' + +export class User { + constructor(private id: string) {} +}` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + expect(violations[0].fromLayer).toBe(LAYERS.DOMAIN) + expect(violations[0].toLayer).toBe(LAYERS.APPLICATION) + expect(violations[0].importPath).toBe("../../application/dtos/UserDto") + expect(violations[0].line).toBe(2) + }) + + it("should detect domain importing from infrastructure", () => { + const code = ` +import { PrismaClient } from '../../infrastructure/database/PrismaClient' + +export class UserRepository { + constructor(private prisma: PrismaClient) {} +}` + const violations = detector.detectViolations( + code, + "src/domain/repositories/UserRepository.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + expect(violations[0].fromLayer).toBe(LAYERS.DOMAIN) + expect(violations[0].toLayer).toBe(LAYERS.INFRASTRUCTURE) + expect(violations[0].importPath).toBe("../../infrastructure/database/PrismaClient") + }) + + it("should NOT detect domain importing from domain", () => { + const code = ` +import { Email } from '../value-objects/Email' +import { UserId } from '../value-objects/UserId' + +export class User { + constructor( + private id: UserId, + private email: Email + ) {} +}` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(0) + }) + + it("should NOT detect domain importing from shared", () => { + const code = ` +import { Result } from '../../shared/types/Result' + +export class User { + static create(id: string): Result { + return Result.ok(new User(id)) + } +}` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(0) + }) + }) + + describe("Application layer violations", () => { + it("should detect application importing from infrastructure", () => { + const code = ` +import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService' + +export class SendWelcomeEmail { + constructor(private emailService: SmtpEmailService) {} +}` + const violations = detector.detectViolations( + code, + "src/application/use-cases/SendWelcomeEmail.ts", + LAYERS.APPLICATION, + ) + + expect(violations).toHaveLength(1) + expect(violations[0].fromLayer).toBe(LAYERS.APPLICATION) + expect(violations[0].toLayer).toBe(LAYERS.INFRASTRUCTURE) + expect(violations[0].getMessage()).toContain( + "Application layer should not import from Infrastructure layer", + ) + }) + + it("should NOT detect application importing from domain", () => { + const code = ` +import { User } from '../../domain/entities/User' +import { IUserRepository } from '../../domain/repositories/IUserRepository' + +export class CreateUser { + constructor(private userRepo: IUserRepository) {} +}` + const violations = detector.detectViolations( + code, + "src/application/use-cases/CreateUser.ts", + LAYERS.APPLICATION, + ) + + expect(violations).toHaveLength(0) + }) + + it("should NOT detect application importing from application", () => { + const code = ` +import { UserResponseDto } from '../dtos/UserResponseDto' +import { UserMapper } from '../mappers/UserMapper' + +export class GetUser { + execute(id: string): UserResponseDto { + return UserMapper.toDto(user) + } +}` + const violations = detector.detectViolations( + code, + "src/application/use-cases/GetUser.ts", + LAYERS.APPLICATION, + ) + + expect(violations).toHaveLength(0) + }) + + it("should NOT detect application importing from shared", () => { + const code = ` +import { Result } from '../../shared/types/Result' + +export class CreateUser { + execute(): Result { + return Result.ok(user) + } +}` + const violations = detector.detectViolations( + code, + "src/application/use-cases/CreateUser.ts", + LAYERS.APPLICATION, + ) + + expect(violations).toHaveLength(0) + }) + }) + + describe("Infrastructure layer", () => { + it("should NOT detect infrastructure importing from domain", () => { + const code = ` +import { User } from '../../domain/entities/User' +import { IUserRepository } from '../../domain/repositories/IUserRepository' + +export class PrismaUserRepository implements IUserRepository { + async save(user: User): Promise {} +}` + const violations = detector.detectViolations( + code, + "src/infrastructure/repositories/PrismaUserRepository.ts", + LAYERS.INFRASTRUCTURE, + ) + + expect(violations).toHaveLength(0) + }) + + it("should NOT detect infrastructure importing from application", () => { + const code = ` +import { CreateUser } from '../../application/use-cases/CreateUser' +import { UserResponseDto } from '../../application/dtos/UserResponseDto' + +export class UserController { + constructor(private createUser: CreateUser) {} +}` + const violations = detector.detectViolations( + code, + "src/infrastructure/controllers/UserController.ts", + LAYERS.INFRASTRUCTURE, + ) + + expect(violations).toHaveLength(0) + }) + + it("should NOT detect infrastructure importing from infrastructure", () => { + const code = ` +import { DatabaseConnection } from '../database/DatabaseConnection' + +export class PrismaUserRepository { + constructor(private db: DatabaseConnection) {} +}` + const violations = detector.detectViolations( + code, + "src/infrastructure/repositories/PrismaUserRepository.ts", + LAYERS.INFRASTRUCTURE, + ) + + expect(violations).toHaveLength(0) + }) + }) + + describe("Multiple violations", () => { + it("should detect multiple violations in same file", () => { + const code = ` +import { UserDto } from '../../application/dtos/UserDto' +import { EmailService } from '../../infrastructure/email/EmailService' +import { Logger } from '../../infrastructure/logging/Logger' + +export class User { + constructor() {} +}` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(3) + expect(violations[0].toLayer).toBe(LAYERS.APPLICATION) + expect(violations[1].toLayer).toBe(LAYERS.INFRASTRUCTURE) + expect(violations[2].toLayer).toBe(LAYERS.INFRASTRUCTURE) + }) + }) + + describe("Import statement formats", () => { + it("should detect violations in named imports", () => { + const code = `import { UserDto, UserRequest } from '../../application/dtos/UserDto'` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + }) + + it("should detect violations in default imports", () => { + const code = `import UserDto from '../../application/dtos/UserDto'` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + }) + + it("should detect violations in namespace imports", () => { + const code = `import * as Dtos from '../../application/dtos'` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + }) + + it("should detect violations in require statements", () => { + const code = `const UserDto = require('../../application/dtos/UserDto')` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(1) + }) + }) + + describe("Edge cases", () => { + it("should return empty array for shared layer", () => { + const code = ` +import { User } from '../../domain/entities/User' +import { CreateUser } from '../../application/use-cases/CreateUser' +` + const violations = detector.detectViolations( + code, + "src/shared/types/Result.ts", + LAYERS.SHARED, + ) + + expect(violations).toHaveLength(0) + }) + + it("should return empty array for undefined layer", () => { + const code = `import { UserDto } from '../../application/dtos/UserDto'` + const violations = detector.detectViolations(code, "src/utils/helper.ts", undefined) + + expect(violations).toHaveLength(0) + }) + + it("should handle empty code", () => { + const violations = detector.detectViolations( + "", + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations).toHaveLength(0) + }) + }) + + describe("getMessage", () => { + it("should return correct message for domain -> application violation", () => { + const code = `import { UserDto } from '../../application/dtos/UserDto'` + const violations = detector.detectViolations( + code, + "src/domain/entities/User.ts", + LAYERS.DOMAIN, + ) + + expect(violations[0].getMessage()).toBe( + "Domain layer should not import from Application layer", + ) + }) + + it("should return correct message for application -> infrastructure violation", () => { + const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'` + const violations = detector.detectViolations( + code, + "src/application/use-cases/SendEmail.ts", + LAYERS.APPLICATION, + ) + + expect(violations[0].getMessage()).toBe( + "Application layer should not import from Infrastructure layer", + ) + }) + }) + + describe("getSuggestion", () => { + it("should return suggestions for domain layer violations", () => { + const code = `import { PrismaClient } from '../../infrastructure/database'` + const violations = detector.detectViolations( + code, + "src/domain/services/UserService.ts", + LAYERS.DOMAIN, + ) + + const suggestion = violations[0].getSuggestion() + expect(suggestion).toContain("Domain layer should be independent") + expect(suggestion).toContain("dependency inversion") + }) + + it("should return suggestions for application layer violations", () => { + const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'` + const violations = detector.detectViolations( + code, + "src/application/use-cases/SendEmail.ts", + LAYERS.APPLICATION, + ) + + const suggestion = violations[0].getSuggestion() + expect(suggestion).toContain( + "Application layer should not depend on infrastructure", + ) + expect(suggestion).toContain("Port") + expect(suggestion).toContain("Adapter") + }) + }) + + describe("getExampleFix", () => { + it("should return example fix for domain -> infrastructure violation", () => { + const code = `import { PrismaClient } from '../../infrastructure/database'` + const violations = detector.detectViolations( + code, + "src/domain/services/UserService.ts", + LAYERS.DOMAIN, + ) + + const example = violations[0].getExampleFix() + expect(example).toContain("// ❌ Bad") + expect(example).toContain("// βœ… Good") + expect(example).toContain("IUserRepository") + }) + + it("should return example fix for application -> infrastructure violation", () => { + const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'` + const violations = detector.detectViolations( + code, + "src/application/use-cases/SendEmail.ts", + LAYERS.APPLICATION, + ) + + const example = violations[0].getExampleFix() + expect(example).toContain("// ❌ Bad") + expect(example).toContain("// βœ… Good") + expect(example).toContain("IEmailService") + }) + }) + }) +})