Files
puaros/packages/guardian/src/infrastructure/analyzers/AggregateBoundaryDetector.ts
imfozilbek 8d400c9517 refactor: extract detector logic into focused strategy classes
Refactored three largest detectors to improve maintainability and reduce complexity:

- AggregateBoundaryDetector: 381 → 162 lines (57% reduction)
- HardcodeDetector: 459 → 89 lines (81% reduction)
- RepositoryPatternDetector: 479 → 106 lines (78% reduction)

Added 13 new strategy classes:
- FolderRegistry - centralized DDD folder name management
- AggregatePathAnalyzer - path parsing and aggregate extraction
- ImportValidator - import validation logic
- BraceTracker - brace and bracket counting
- ConstantsFileChecker - constants file detection
- ExportConstantAnalyzer - export const analysis
- MagicNumberMatcher - magic number detection
- MagicStringMatcher - magic string detection
- OrmTypeMatcher - ORM type matching
- MethodNameValidator - repository method validation
- RepositoryFileAnalyzer - file role detection
- RepositoryViolationDetector - violation detection logic

All 519 tests passing, zero ESLint errors, no breaking changes.
2025-11-25 17:41:32 +05:00

168 lines
5.6 KiB
TypeScript

import { IAggregateBoundaryDetector } from "../../domain/services/IAggregateBoundaryDetector"
import { AggregateBoundaryViolation } from "../../domain/value-objects/AggregateBoundaryViolation"
import { LAYERS } from "../../shared/constants/rules"
import { AggregatePathAnalyzer } from "../strategies/AggregatePathAnalyzer"
import { FolderRegistry } from "../strategies/FolderRegistry"
import { ImportValidator } from "../strategies/ImportValidator"
/**
* Detects aggregate boundary violations in Domain-Driven Design
*
* This detector enforces DDD aggregate rules:
* - Aggregates should reference each other only by ID or Value Objects
* - Direct entity references across aggregates create tight coupling
* - Each aggregate should be independently modifiable
*
* Folder structure patterns detected:
* - domain/aggregates/order/Order.ts
* - domain/order/Order.ts (aggregate name from parent folder)
* - domain/entities/order/Order.ts
*
* @example
* ```typescript
* const detector = new AggregateBoundaryDetector()
*
* // Detect violations in order aggregate
* const code = `
* import { User } from '../user/User'
* import { UserId } from '../user/value-objects/UserId'
* `
* const violations = detector.detectViolations(
* code,
* 'src/domain/aggregates/order/Order.ts',
* 'domain'
* )
*
* // violations will contain 1 violation for direct User entity import
* // but not for UserId (value object is allowed)
* console.log(violations.length) // 1
* ```
*/
export class AggregateBoundaryDetector implements IAggregateBoundaryDetector {
private readonly folderRegistry: FolderRegistry
private readonly pathAnalyzer: AggregatePathAnalyzer
private readonly importValidator: ImportValidator
constructor() {
this.folderRegistry = new FolderRegistry()
this.pathAnalyzer = new AggregatePathAnalyzer(this.folderRegistry)
this.importValidator = new ImportValidator(this.folderRegistry, this.pathAnalyzer)
}
/**
* Detects aggregate boundary violations in the given code
*
* Analyzes import statements to identify direct entity references
* across aggregate boundaries in the domain layer.
*
* @param code - Source code to analyze
* @param filePath - Path to the file being analyzed
* @param layer - The architectural layer of the file (should be 'domain')
* @returns Array of detected aggregate boundary violations
*/
public detectViolations(
code: string,
filePath: string,
layer: string | undefined,
): AggregateBoundaryViolation[] {
if (layer !== LAYERS.DOMAIN) {
return []
}
const currentAggregate = this.pathAnalyzer.extractAggregateFromPath(filePath)
if (!currentAggregate) {
return []
}
return this.analyzeImports(code, filePath, currentAggregate)
}
/**
* Checks if a file path belongs to an aggregate
*
* Extracts aggregate name from paths like:
* - domain/aggregates/order/Order.ts → 'order'
* - domain/order/Order.ts → 'order'
* - domain/entities/order/Order.ts → 'order'
*
* @param filePath - The file path to check
* @returns The aggregate name if found, undefined otherwise
*/
public extractAggregateFromPath(filePath: string): string | undefined {
return this.pathAnalyzer.extractAggregateFromPath(filePath)
}
/**
* Checks if an import path references an entity from another aggregate
*
* @param importPath - The import path to analyze
* @param currentAggregate - The aggregate of the current file
* @returns True if the import crosses aggregate boundaries inappropriately
*/
public isAggregateBoundaryViolation(importPath: string, currentAggregate: string): boolean {
return this.importValidator.isViolation(importPath, currentAggregate)
}
/**
* Analyzes all imports in code and detects violations
*/
private analyzeImports(
code: string,
filePath: string,
currentAggregate: string,
): AggregateBoundaryViolation[] {
const violations: AggregateBoundaryViolation[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
const imports = this.importValidator.extractImports(line)
for (const importPath of imports) {
const violation = this.checkImport(
importPath,
currentAggregate,
filePath,
lineNumber,
)
if (violation) {
violations.push(violation)
}
}
}
return violations
}
/**
* Checks a single import for boundary violations
*/
private checkImport(
importPath: string,
currentAggregate: string,
filePath: string,
lineNumber: number,
): AggregateBoundaryViolation | undefined {
if (!this.importValidator.isViolation(importPath, currentAggregate)) {
return undefined
}
const targetAggregate = this.pathAnalyzer.extractAggregateFromImport(importPath)
const entityName = this.pathAnalyzer.extractEntityName(importPath)
if (targetAggregate && entityName) {
return AggregateBoundaryViolation.create(
currentAggregate,
targetAggregate,
entityName,
importPath,
filePath,
lineNumber,
)
}
return undefined
}
}