/** * @angular-modernizer/plugin-angular - Boundary Rules * * Concrete analysis rules that detect architectural violations using the * dependency graph and architecture analyzer. */ // TODO: Break up into individual rules? import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; import { DependencyGraph } from './dependency-graph.js'; import { ArchitectureAnalyzer, ViolationType, } from './architecture-analyzer.js'; /** * Layer Violation Rule - detects when modules import from forbidden layers */ export class LayerViolationRule implements AnalysisRule { public readonly id = 'angular:architecture-layer-violation'; public readonly name = 'Layer Violation'; public readonly description = 'Detects when modules import from forbidden architectural layers'; public readonly severity = 'error'; public readonly category = 'angular-architecture'; public readonly tags = ['angular', 'architecture', 'layers', 'dependencies']; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { // Build dependency graph once for the entire project if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeLayerViolations(); return violations.map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Circular Dependency Rule - detects circular dependencies between modules */ export class CircularDependencyRule implements AnalysisRule { public readonly id = 'angular:architecture-circular-dependency'; public readonly name = 'Circular Dependency'; public readonly description = 'Detects circular dependencies between modules'; public readonly severity = 'error'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'circular', 'dependencies', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeCircularDependencies(); return violations.map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * God Object Rule - detects classes with too many responsibilities */ export class GodObjectRule implements AnalysisRule { public readonly id = 'angular:architecture-god-object'; public readonly name = 'God Object'; public readonly description = 'Detects classes with too many methods (God objects)'; public readonly severity = 'warning'; public readonly category = 'angular-architecture'; public readonly tags = ['angular', 'architecture', 'solid', 'god-object']; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeGodObjects(context); return violations.map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Single Responsibility Rule - detects classes with multiple responsibilities */ export class SingleResponsibilityRule implements AnalysisRule { public readonly id = 'angular:architecture-single-responsibility'; public readonly name = 'Single Responsibility Violation'; public readonly description = 'Detects classes that violate the Single Responsibility Principle'; public readonly severity = 'warning'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'solid', 'single-responsibility', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeSolidViolations(context); return violations .filter((v) => v.type === ViolationType.SINGLE_RESPONSIBILITY) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Open Closed Rule - detects classes that need modification for extension */ export class OpenClosedRule implements AnalysisRule { public readonly id = 'angular:architecture-open-closed'; public readonly name = 'Open/Closed Principle Violation'; public readonly description = 'Detects classes that violate the Open/Closed Principle'; public readonly severity = 'info'; public readonly category = 'angular-architecture'; public readonly tags = ['angular', 'architecture', 'solid', 'open-closed']; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeSolidViolations(context); return violations .filter((v) => v.type === ViolationType.OPEN_CLOSED) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Direct Instantiation Rule - detects direct service instantiation */ export class DirectInstantiationRule implements AnalysisRule { public readonly id = 'angular:architecture-direct-instantiation'; public readonly name = 'Direct Service Instantiation'; public readonly description = 'Detects direct instantiation of services instead of dependency injection'; public readonly severity = 'warning'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'dependency-injection', 'services', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeServicePatterns(context); return violations .filter((v) => v.type === ViolationType.DIRECT_INSTANTIATION) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Missing Interface Rule - detects services without interfaces */ export class MissingInterfaceRule implements AnalysisRule { public readonly id = 'angular:architecture-missing-interface'; public readonly name = 'Missing Service Interface'; public readonly description = "Detects services that don't implement interfaces"; public readonly severity = 'info'; public readonly category = 'angular-architecture'; public readonly tags = ['angular', 'architecture', 'interfaces', 'services']; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeServicePatterns(context); return violations .filter((v) => v.type === ViolationType.MISSING_INTERFACE) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Service Locator Rule - detects service locator pattern usage */ export class ServiceLocatorRule implements AnalysisRule { public readonly id = 'angular:architecture-service-locator'; public readonly name = 'Service Locator Pattern'; public readonly description = 'Detects usage of the Service Locator anti-pattern'; public readonly severity = 'warning'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'anti-pattern', 'service-locator', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeServicePatterns(context); return violations .filter((v) => v.type === ViolationType.SERVICE_LOCATOR) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Dumb Component Business Logic Rule - detects presentation components with business logic */ export class DumbComponentBusinessLogicRule implements AnalysisRule { public readonly id = 'angular:architecture-dumb-component-business-logic'; public readonly name = 'Dumb Component Business Logic'; public readonly description = 'Detects presentation components that contain business logic'; public readonly severity = 'warning'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'components', 'business-logic', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeComponentPatterns(context); return violations .filter((v) => v.type === ViolationType.DUMB_SMART_VIOLATION) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } } /** * Smart Component Presentation Rule - detects container components with presentation logic */ export class SmartComponentPresentationRule implements AnalysisRule { public readonly id = 'angular:architecture-smart-component-presentation'; public readonly name = 'Smart Component Presentation Logic'; public readonly description = 'Detects container components with complex presentation logic'; public readonly severity = 'info'; public readonly category = 'angular-architecture'; public readonly tags = [ 'angular', 'architecture', 'components', 'presentation', ]; private dependencyGraph: DependencyGraph | null = null; private architectureAnalyzer: ArchitectureAnalyzer | null = null; async initialize(context: AnalysisContext): Promise { if (!this.dependencyGraph) { this.dependencyGraph = new DependencyGraph(); const sourceFiles = context.project.getSourceFiles(); this.dependencyGraph.build(sourceFiles); this.architectureAnalyzer = new ArchitectureAnalyzer( this.dependencyGraph, ); } } async analyze(context: AnalysisContext): Promise { if (!this.architectureAnalyzer) { await this.initialize(context); } const violations = this.architectureAnalyzer!.analyzeComponentPatterns(context); return violations .filter((v) => v.type === ViolationType.SMART_DUMB_VIOLATION) .map((violation) => ({ ruleId: this.id, message: violation.description, filePath: violation.location.file, line: violation.location.line, column: violation.location.column, suggestedFix: violation.suggestedFix, metadata: violation.metadata, })); } }