Files
puaros/packages/guardian/src/infrastructure/analyzers/DependencyDirectionDetector.ts
imfozilbek a34ca85241 chore: refactor hardcoded values to constants (v0.5.1)
Major internal refactoring to eliminate hardcoded values and improve
maintainability. Guardian now fully passes its own quality checks!

Changes:
- Extract all RepositoryViolation messages to domain constants
- Extract all framework leak template strings to centralized constants
- Extract all layer paths to infrastructure constants
- Extract all regex patterns to IMPORT_PATTERNS constant
- Add 30+ new constants for better maintainability

New files:
- src/infrastructure/constants/paths.ts (layer paths, patterns)
- src/domain/constants/Messages.ts (25+ repository messages)
- src/domain/constants/FrameworkCategories.ts (framework categories)
- src/shared/constants/layers.ts (layer names)

Impact:
- Reduced hardcoded values from 37 to 1 (97% improvement)
- Guardian passes its own src/ directory checks with 0 violations
- All 292 tests still passing (100% pass rate)
- No breaking changes - fully backwards compatible

Test results:
- 292 tests passing (100% pass rate)
- 96.77% statement coverage
- 83.82% branch coverage
2025-11-24 20:12:08 +05:00

182 lines
6.1 KiB
TypeScript

import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
import { DependencyViolation } from "../../domain/value-objects/DependencyViolation"
import { LAYERS } from "../../shared/constants/rules"
import { IMPORT_PATTERNS, LAYER_PATHS } from "../constants/paths"
/**
* 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(IMPORT_PATTERNS.QUOTE, "").toLowerCase()
const layerPatterns: [string, string][] = [
[LAYERS.DOMAIN, LAYER_PATHS.DOMAIN],
[LAYERS.APPLICATION, LAYER_PATHS.APPLICATION],
[LAYERS.INFRASTRUCTURE, LAYER_PATHS.INFRASTRUCTURE],
[LAYERS.SHARED, LAYER_PATHS.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[] = []
let match = IMPORT_PATTERNS.ES_IMPORT.exec(line)
while (match) {
imports.push(match[1])
match = IMPORT_PATTERNS.ES_IMPORT.exec(line)
}
match = IMPORT_PATTERNS.REQUIRE.exec(line)
while (match) {
imports.push(match[1])
match = IMPORT_PATTERNS.REQUIRE.exec(line)
}
return imports
}
}