mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +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:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This document outlines the current features and future plans for @puaros/guardian.
|
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
|
**Released:** 2025-11-24
|
||||||
|
|
||||||
@@ -71,10 +71,9 @@ async getUser(id: string): Promise<UserResponseDto> {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Future Roadmap
|
## Version 0.4.0 - Dependency Direction Enforcement 🎯 ✅ RELEASED
|
||||||
|
|
||||||
### Version 0.4.0 - Dependency Direction Enforcement 🎯
|
**Released:** 2025-11-24
|
||||||
**Target:** Q1 2026
|
|
||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
|
|
||||||
Enforce correct dependency direction between architectural layers:
|
Enforce correct dependency direction between architectural layers:
|
||||||
@@ -103,16 +102,20 @@ import { User } from '../../domain/entities/User' // OK
|
|||||||
- ✅ Infrastructure → Application, Domain (разрешено)
|
- ✅ Infrastructure → Application, Domain (разрешено)
|
||||||
- ✅ Shared → используется везде
|
- ✅ Shared → используется везде
|
||||||
|
|
||||||
**Planned Features:**
|
**Implemented Features:**
|
||||||
- Detect domain importing from application
|
- ✅ Detect domain importing from application
|
||||||
- Detect domain importing from infrastructure
|
- ✅ Detect domain importing from infrastructure
|
||||||
- Detect application importing from infrastructure
|
- ✅ Detect application importing from infrastructure
|
||||||
- Visualize dependency graph
|
- ✅ Detect violations in all import formats (ES6, require)
|
||||||
- Suggest refactoring to fix violations
|
- ✅ Provide detailed error messages with suggestions
|
||||||
- Support for custom layer definitions
|
- ✅ 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 📚
|
### Version 0.5.0 - Repository Pattern Validation 📚
|
||||||
**Target:** Q1 2026
|
**Target:** Q1 2026
|
||||||
**Priority:** HIGH
|
**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
|
**Last Updated:** 2025-11-24
|
||||||
**Current Version:** 0.3.0
|
**Current Version:** 0.4.0
|
||||||
|
|||||||
@@ -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<User> {
|
||||||
|
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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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<User | null> {
|
||||||
|
return await this.userRepo.findById(UserId.from(userId))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<User> {
|
||||||
|
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<User> {
|
||||||
|
const user = await this.userService.createUser(email)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ❌ VIOLATION: Domain calling infrastructure service directly
|
||||||
|
*/
|
||||||
|
await this.emailService.sendWelcomeEmail(email)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Result<UserResponseDto>> {
|
||||||
|
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<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendWelcomeEmail {
|
||||||
|
constructor(
|
||||||
|
private readonly userRepo: IUserRepository,
|
||||||
|
private readonly emailService: IEmailService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(userId: string): Promise<Result<void>> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<User | null>
|
||||||
|
save(user: User): Promise<void>
|
||||||
|
delete(id: UserId): Promise<void>
|
||||||
|
}
|
||||||
@@ -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<string, User> = new Map()
|
||||||
|
|
||||||
|
async findById(id: UserId): Promise<User | null> {
|
||||||
|
return this.users.get(id.getValue()) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(user: User): Promise<void> {
|
||||||
|
this.users.set(user.getId().getValue(), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: UserId): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<UserResponseDto> {
|
||||||
|
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<User | null> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
await this.prisma.user.delete({
|
||||||
|
where: { id: id.getValue() },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/guardian",
|
"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.",
|
"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": [
|
"keywords": [
|
||||||
"puaros",
|
"puaros",
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { IHardcodeDetector } from "./domain/services/IHardcodeDetector"
|
|||||||
import { INamingConventionDetector } from "./domain/services/INamingConventionDetector"
|
import { INamingConventionDetector } from "./domain/services/INamingConventionDetector"
|
||||||
import { IFrameworkLeakDetector } from "./domain/services/IFrameworkLeakDetector"
|
import { IFrameworkLeakDetector } from "./domain/services/IFrameworkLeakDetector"
|
||||||
import { IEntityExposureDetector } from "./domain/services/IEntityExposureDetector"
|
import { IEntityExposureDetector } from "./domain/services/IEntityExposureDetector"
|
||||||
|
import { IDependencyDirectionDetector } from "./domain/services/IDependencyDirectionDetector"
|
||||||
import { FileScanner } from "./infrastructure/scanners/FileScanner"
|
import { FileScanner } from "./infrastructure/scanners/FileScanner"
|
||||||
import { CodeParser } from "./infrastructure/parsers/CodeParser"
|
import { CodeParser } from "./infrastructure/parsers/CodeParser"
|
||||||
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
|
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
|
||||||
import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConventionDetector"
|
import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConventionDetector"
|
||||||
import { FrameworkLeakDetector } from "./infrastructure/analyzers/FrameworkLeakDetector"
|
import { FrameworkLeakDetector } from "./infrastructure/analyzers/FrameworkLeakDetector"
|
||||||
import { EntityExposureDetector } from "./infrastructure/analyzers/EntityExposureDetector"
|
import { EntityExposureDetector } from "./infrastructure/analyzers/EntityExposureDetector"
|
||||||
|
import { DependencyDirectionDetector } from "./infrastructure/analyzers/DependencyDirectionDetector"
|
||||||
import { ERROR_MESSAGES } from "./shared/constants"
|
import { ERROR_MESSAGES } from "./shared/constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +71,8 @@ export async function analyzeProject(
|
|||||||
const namingConventionDetector: INamingConventionDetector = new NamingConventionDetector()
|
const namingConventionDetector: INamingConventionDetector = new NamingConventionDetector()
|
||||||
const frameworkLeakDetector: IFrameworkLeakDetector = new FrameworkLeakDetector()
|
const frameworkLeakDetector: IFrameworkLeakDetector = new FrameworkLeakDetector()
|
||||||
const entityExposureDetector: IEntityExposureDetector = new EntityExposureDetector()
|
const entityExposureDetector: IEntityExposureDetector = new EntityExposureDetector()
|
||||||
|
const dependencyDirectionDetector: IDependencyDirectionDetector =
|
||||||
|
new DependencyDirectionDetector()
|
||||||
const useCase = new AnalyzeProject(
|
const useCase = new AnalyzeProject(
|
||||||
fileScanner,
|
fileScanner,
|
||||||
codeParser,
|
codeParser,
|
||||||
@@ -76,6 +80,7 @@ export async function analyzeProject(
|
|||||||
namingConventionDetector,
|
namingConventionDetector,
|
||||||
frameworkLeakDetector,
|
frameworkLeakDetector,
|
||||||
entityExposureDetector,
|
entityExposureDetector,
|
||||||
|
dependencyDirectionDetector,
|
||||||
)
|
)
|
||||||
|
|
||||||
const result = await useCase.execute(options)
|
const result = await useCase.execute(options)
|
||||||
@@ -96,5 +101,6 @@ export type {
|
|||||||
NamingConventionViolation,
|
NamingConventionViolation,
|
||||||
FrameworkLeakViolation,
|
FrameworkLeakViolation,
|
||||||
EntityExposureViolation,
|
EntityExposureViolation,
|
||||||
|
DependencyDirectionViolation,
|
||||||
ProjectMetrics,
|
ProjectMetrics,
|
||||||
} from "./application/use-cases/AnalyzeProject"
|
} from "./application/use-cases/AnalyzeProject"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
|
|||||||
import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector"
|
import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector"
|
||||||
import { IFrameworkLeakDetector } from "../../domain/services/IFrameworkLeakDetector"
|
import { IFrameworkLeakDetector } from "../../domain/services/IFrameworkLeakDetector"
|
||||||
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
|
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
|
||||||
|
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
|
||||||
import { SourceFile } from "../../domain/entities/SourceFile"
|
import { SourceFile } from "../../domain/entities/SourceFile"
|
||||||
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
|
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
|
||||||
import { ProjectPath } from "../../domain/value-objects/ProjectPath"
|
import { ProjectPath } from "../../domain/value-objects/ProjectPath"
|
||||||
@@ -34,6 +35,7 @@ export interface AnalyzeProjectResponse {
|
|||||||
namingViolations: NamingConventionViolation[]
|
namingViolations: NamingConventionViolation[]
|
||||||
frameworkLeakViolations: FrameworkLeakViolation[]
|
frameworkLeakViolations: FrameworkLeakViolation[]
|
||||||
entityExposureViolations: EntityExposureViolation[]
|
entityExposureViolations: EntityExposureViolation[]
|
||||||
|
dependencyDirectionViolations: DependencyDirectionViolation[]
|
||||||
metrics: ProjectMetrics
|
metrics: ProjectMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,17 @@ export interface EntityExposureViolation {
|
|||||||
suggestion: string
|
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 {
|
export interface ProjectMetrics {
|
||||||
totalFiles: number
|
totalFiles: number
|
||||||
totalFunctions: number
|
totalFunctions: number
|
||||||
@@ -130,6 +143,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
private readonly namingConventionDetector: INamingConventionDetector,
|
private readonly namingConventionDetector: INamingConventionDetector,
|
||||||
private readonly frameworkLeakDetector: IFrameworkLeakDetector,
|
private readonly frameworkLeakDetector: IFrameworkLeakDetector,
|
||||||
private readonly entityExposureDetector: IEntityExposureDetector,
|
private readonly entityExposureDetector: IEntityExposureDetector,
|
||||||
|
private readonly dependencyDirectionDetector: IDependencyDirectionDetector,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -180,6 +194,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
const namingViolations = this.detectNamingConventions(sourceFiles)
|
const namingViolations = this.detectNamingConventions(sourceFiles)
|
||||||
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
|
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
|
||||||
const entityExposureViolations = this.detectEntityExposures(sourceFiles)
|
const entityExposureViolations = this.detectEntityExposures(sourceFiles)
|
||||||
|
const dependencyDirectionViolations = this.detectDependencyDirections(sourceFiles)
|
||||||
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
|
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
|
||||||
|
|
||||||
return ResponseDto.ok({
|
return ResponseDto.ok({
|
||||||
@@ -191,6 +206,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
namingViolations,
|
namingViolations,
|
||||||
frameworkLeakViolations,
|
frameworkLeakViolations,
|
||||||
entityExposureViolations,
|
entityExposureViolations,
|
||||||
|
dependencyDirectionViolations,
|
||||||
metrics,
|
metrics,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -409,6 +425,33 @@ export class AnalyzeProject extends UseCase<
|
|||||||
return violations
|
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(
|
private calculateMetrics(
|
||||||
sourceFiles: SourceFile[],
|
sourceFiles: SourceFile[],
|
||||||
totalFunctions: number,
|
totalFunctions: number,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, Set<string>>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export const RULES = {
|
|||||||
NAMING_CONVENTION: "naming-convention",
|
NAMING_CONVENTION: "naming-convention",
|
||||||
FRAMEWORK_LEAK: "framework-leak",
|
FRAMEWORK_LEAK: "framework-leak",
|
||||||
ENTITY_EXPOSURE: "entity-exposure",
|
ENTITY_EXPOSURE: "entity-exposure",
|
||||||
|
DEPENDENCY_DIRECTION: "dependency-direction",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
511
packages/guardian/tests/DependencyDirectionDetector.test.ts
Normal file
511
packages/guardian/tests/DependencyDirectionDetector.test.ts
Normal file
@@ -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<User> {
|
||||||
|
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<User> {
|
||||||
|
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<void> {}
|
||||||
|
}`
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user