Files
puaros/packages/guardian/src/infrastructure/strategies/AggregatePathAnalyzer.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

178 lines
5.5 KiB
TypeScript

import { DDD_FOLDER_NAMES } from "../constants/detectorPatterns"
import { IMPORT_PATTERNS } from "../constants/paths"
import { FolderRegistry } from "./FolderRegistry"
/**
* Analyzes file paths and imports to extract aggregate information
*
* Handles path normalization, aggregate extraction, and entity name detection
* for aggregate boundary validation.
*/
export class AggregatePathAnalyzer {
constructor(private readonly folderRegistry: FolderRegistry) {}
/**
* Extracts the aggregate name from a file path
*
* Handles patterns like:
* - domain/aggregates/order/Order.ts → 'order'
* - domain/order/Order.ts → 'order'
* - domain/entities/order/Order.ts → 'order'
*/
public extractAggregateFromPath(filePath: string): string | undefined {
const normalizedPath = this.normalizePath(filePath)
const segments = this.getPathSegmentsAfterDomain(normalizedPath)
if (!segments || segments.length < 2) {
return undefined
}
return this.findAggregateInSegments(segments)
}
/**
* Extracts the aggregate name from an import path
*/
public extractAggregateFromImport(importPath: string): string | undefined {
const normalizedPath = importPath.replace(IMPORT_PATTERNS.QUOTE, "").toLowerCase()
const segments = normalizedPath.split("/").filter((seg) => seg !== ".." && seg !== ".")
if (segments.length === 0) {
return undefined
}
return this.findAggregateInImportSegments(segments)
}
/**
* Extracts the entity name from an import path
*/
public extractEntityName(importPath: string): string | undefined {
const normalizedPath = importPath.replace(IMPORT_PATTERNS.QUOTE, "")
const segments = normalizedPath.split("/")
const lastSegment = segments[segments.length - 1]
if (lastSegment) {
return lastSegment.replace(/\.(ts|js)$/, "")
}
return undefined
}
/**
* Normalizes a file path for consistent processing
*/
private normalizePath(filePath: string): string {
return filePath.toLowerCase().replace(/\\/g, "/")
}
/**
* Gets path segments after the 'domain' folder
*/
private getPathSegmentsAfterDomain(normalizedPath: string): string[] | undefined {
const domainMatch = /(?:^|\/)(domain)\//.exec(normalizedPath)
if (!domainMatch) {
return undefined
}
const domainEndIndex = domainMatch.index + domainMatch[0].length
const pathAfterDomain = normalizedPath.substring(domainEndIndex)
return pathAfterDomain.split("/").filter(Boolean)
}
/**
* Finds aggregate name in path segments after domain folder
*/
private findAggregateInSegments(segments: string[]): string | undefined {
if (this.folderRegistry.isEntityFolder(segments[0])) {
return this.extractFromEntityFolder(segments)
}
const aggregate = segments[0]
if (this.folderRegistry.isNonAggregateFolder(aggregate)) {
return undefined
}
return aggregate
}
/**
* Extracts aggregate from entity folder structure
*/
private extractFromEntityFolder(segments: string[]): string | undefined {
if (segments.length < 3) {
return undefined
}
const aggregate = segments[1]
if (this.folderRegistry.isNonAggregateFolder(aggregate)) {
return undefined
}
return aggregate
}
/**
* Finds aggregate in import path segments
*/
private findAggregateInImportSegments(segments: string[]): string | undefined {
const aggregateFromDomainFolder = this.findAggregateAfterDomainFolder(segments)
if (aggregateFromDomainFolder) {
return aggregateFromDomainFolder
}
return this.findAggregateFromSecondLastSegment(segments)
}
/**
* Finds aggregate after 'domain' or 'aggregates' folder in import
*/
private findAggregateAfterDomainFolder(segments: string[]): string | undefined {
for (let i = 0; i < segments.length; i++) {
const isDomainOrAggregatesFolder =
segments[i] === DDD_FOLDER_NAMES.DOMAIN ||
segments[i] === DDD_FOLDER_NAMES.AGGREGATES
if (!isDomainOrAggregatesFolder) {
continue
}
if (i + 1 >= segments.length) {
continue
}
const nextSegment = segments[i + 1]
const isEntityOrAggregateFolder =
this.folderRegistry.isEntityFolder(nextSegment) ||
nextSegment === DDD_FOLDER_NAMES.AGGREGATES
if (isEntityOrAggregateFolder) {
return i + 2 < segments.length ? segments[i + 2] : undefined
}
return nextSegment
}
return undefined
}
/**
* Extracts aggregate from second-to-last segment if applicable
*/
private findAggregateFromSecondLastSegment(segments: string[]): string | undefined {
if (segments.length >= 2) {
const secondLastSegment = segments[segments.length - 2]
if (
!this.folderRegistry.isEntityFolder(secondLastSegment) &&
!this.folderRegistry.isValueObjectFolder(secondLastSegment) &&
!this.folderRegistry.isAllowedFolder(secondLastSegment) &&
secondLastSegment !== DDD_FOLDER_NAMES.DOMAIN
) {
return secondLastSegment
}
}
return undefined
}
}