/** * @angular-modernizer/plugin-angular - Missing OnPush Change Detection Rule * * Detects Angular components that don't use OnPush change detection strategy. * OnPush change detection improves performance by only checking components when * their inputs change or when manually triggered. * * Detection Patterns: * - Components without ChangeDetectionStrategy.OnPush * - Components that should use OnPush but don't * * This rule helps improve Angular application performance. */ import { SyntaxKind, type ClassDeclaration, type Decorator } from 'ts-morph'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; /** * Missing OnPush Change Detection Rule - detects components without OnPush strategy. */ export class MissingOnPushRule implements AnalysisRule { public readonly id = 'angular:missing-onpush-change-detection'; public readonly name = 'Missing OnPush Change Detection'; public readonly description = 'Detects Angular components that should use OnPush change detection strategy'; public readonly severity = 'warning'; public readonly category = 'angular-performance'; public readonly tags = [ 'angular', 'performance', 'change-detection', 'onpush', 'component', ]; async analyze(context: AnalysisContext): Promise { const violations: AnalysisResult[] = []; const sourceFile = context.sourceFile; // Find all class declarations const classes = sourceFile.getDescendantsOfKind( SyntaxKind.ClassDeclaration, ); for (const classDecl of classes) { const violation = this.analyzeComponent( classDecl, sourceFile.getFilePath(), ); if (violation) { violations.push(violation); } } return violations; } private analyzeComponent( classDecl: ClassDeclaration, filePath: string, ): AnalysisResult | null { try { const className = classDecl.getName(); if (!className) { return null; } // Check if this is an Angular component const componentDecorator = this.getComponentDecorator(classDecl); if (!componentDecorator) { return null; } // Check if component already has OnPush strategy if (this.hasOnPushStrategy(classDecl)) { return null; // Already using OnPush, no violation } // Check if component should use OnPush (has inputs or complex logic) if (this.shouldUseOnPush(classDecl)) { const startLine = classDecl.getStartLineNumber(); const startColumn = classDecl.getStart() - classDecl.getStartLinePos(); return { ruleId: this.id, message: `Component '${className}' should use ChangeDetectionStrategy.OnPush for better performance. Components with @Input properties or complex logic benefit from OnPush change detection.`, filePath, line: startLine, column: startColumn, suggestedFix: `Add 'changeDetection: ChangeDetectionStrategy.OnPush' to the @Component decorator and ensure all @Input changes are immutable.`, metadata: { className, componentDecorator: true, hasInputs: this.hasInputs(classDecl), hasComplexLogic: this.hasComplexLogic(classDecl), violationType: 'missing-onpush-strategy', framework: 'angular', }, }; } return null; } catch (_error) { return null; } } private getComponentDecorator( classDecl: ClassDeclaration, ): Decorator | undefined { const decorators = classDecl.getDecorators(); return decorators.find((decorator) => { const decoratorName = decorator.getName(); return decoratorName === 'Component'; }); } private hasOnPushStrategy(classDecl: ClassDeclaration): boolean { const componentDecorator = this.getComponentDecorator(classDecl); if (!componentDecorator) { return false; } try { // Look for changeDetection property in the decorator const decoratorText = componentDecorator.getFullText(); // Simple string check for OnPush strategy return ( decoratorText.includes('ChangeDetectionStrategy.OnPush') || decoratorText.includes('ChangeDetectionStrategy["OnPush"]') || decoratorText.includes("ChangeDetectionStrategy['OnPush']") ); } catch (_error) { return false; } } private shouldUseOnPush(classDecl: ClassDeclaration): boolean { // Components should use OnPush if they have: // 1. @Input properties (most common case) // 2. Complex methods (indicating business logic) // 3. Multiple dependencies (indicating complex component) const hasInputs = this.hasInputs(classDecl); const hasComplexLogic = this.hasComplexLogic(classDecl); const hasManyDependencies = this.hasManyDependencies(classDecl); // If component has inputs, it definitely should use OnPush if (hasInputs) { return true; } // If component has complex logic or many dependencies, consider OnPush if (hasComplexLogic && hasManyDependencies) { return true; } return false; } private hasInputs(classDecl: ClassDeclaration): boolean { const properties = classDecl.getProperties(); return properties.some((prop) => { const decorators = prop.getDecorators(); return decorators.some((decorator) => decorator.getName() === 'Input'); }); } private hasComplexLogic(classDecl: ClassDeclaration): boolean { const methods = classDecl.getMethods(); // Consider complex if: // - More than 5 methods // - Has methods longer than 10 lines // - Has lifecycle methods (ngOnInit, ngOnChanges, etc.) if (methods.length > 5) { return true; } const hasLongMethods = methods.some((method) => { try { const startLine = method.getStartLineNumber(); const endLine = method.getEndLineNumber(); return endLine - startLine > 10; } catch (_error) { return false; } }); if (hasLongMethods) { return true; } // Check for Angular lifecycle methods const lifecycleMethods = new Set([ 'ngOnInit', 'ngOnChanges', 'ngOnDestroy', 'ngAfterViewInit', 'ngAfterContentInit', ]); const hasLifecycleMethods = methods.some((method) => lifecycleMethods.has(method.getName() || ''), ); return hasLifecycleMethods; } private hasManyDependencies(classDecl: ClassDeclaration): boolean { const constructors = classDecl.getConstructors(); if (constructors.length === 0) { return false; } const constructor = constructors[0]; if (!constructor) { return false; } const parameters = constructor.getParameters(); // Consider "many dependencies" as more than 3 constructor parameters return parameters.length > 3; } }