/** * @angular-modernizer/plugin-angular - Missing TrackBy Rule * * Detects ngFor directives without trackBy functions, which can cause performance issues * and unnecessary DOM operations in Angular applications. * * Detection Patterns: * - *ngFor directives without trackBy function * - ngFor loops over large arrays without optimization * - ngFor in performance-critical components * * This rule helps improve Angular application performance by ensuring proper list tracking. */ import { SyntaxKind, type ClassDeclaration } from 'ts-morph'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; /** * Missing TrackBy Rule - detects ngFor without trackBy functions. */ export class MissingTrackByRule implements AnalysisRule { public readonly id = 'angular:missing-trackby'; public readonly name = 'Missing TrackBy Function'; public readonly description = 'Detects ngFor directives without trackBy functions that can cause performance issues'; public readonly severity = 'warning'; public readonly category = 'angular-performance'; public readonly tags = [ 'angular', 'performance', 'ngfor', 'trackby', 'template', ]; async analyze(context: AnalysisContext): Promise { const violations: AnalysisResult[] = []; const sourceFile = context.sourceFile; // Find all class declarations (Angular components) const classes = sourceFile.getDescendantsOfKind( SyntaxKind.ClassDeclaration, ); for (const classDecl of classes) { const classViolations = this.analyzeComponent( classDecl, sourceFile.getFilePath(), ); violations.push(...classViolations); } return violations; } /** * Analyzes a single component for missing trackBy violations. * @param classDecl - The class declaration of the component to analyze. * @param filePath - The file path of the source file. * @returns An array of analysis results for trackBy violations. */ private analyzeComponent( classDecl: ClassDeclaration, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const className = classDecl.getName(); if (!className) { return violations; } // Only analyze Angular components if (!this.isAngularComponent(classDecl)) { return violations; } const templateContent = this.extractTemplateContent(classDecl); if (!templateContent) { return violations; } const ngForViolations = this.analyzeTemplateForNgFor( templateContent, className, filePath, ); violations.push(...ngForViolations); return violations; } /** * Checks if a class declaration is an Angular component. * @param classDecl - The class declaration to check. * @returns True if the class is an Angular component, false otherwise. */ private isAngularComponent(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((decorator) => decorator.getName() === 'Component'); } /** * Extracts the template content from a component decorator. * @param classDecl - The class declaration of the component. * @returns The template content as a string, or null if not found. */ private extractTemplateContent(classDecl: ClassDeclaration): string | null { const componentDecorator = classDecl .getDecorators() .find((d) => d.getName() === 'Component'); if (!componentDecorator) { return null; } try { const decoratorText = componentDecorator.getFullText(); // Extract template from inline template const templateMatch = /template\s*:\s*[`'"]([^`'"]*)[`'"]/.exec( decoratorText, ); if (templateMatch?.[1]) { return templateMatch[1]; } // Skip external templates for now if (decoratorText.includes('templateUrl')) { return null; } return null; } catch (_error) { return null; } } /** * Analyzes template content for ngFor directives missing trackBy functions. * @param templateContent - The template content to analyze. * @param className - The name of the component class. * @param filePath - The file path of the source file. * @returns An array of analysis results for missing trackBy violations. */ private analyzeTemplateForNgFor( templateContent: string, className: string, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const lines = templateContent.split('\n'); lines.forEach((line, lineIndex) => { if (!line.trim()) { return; } const lineNumber = lineIndex + 1; const ngForDirectives = this.extractNgForDirectives(line); ngForDirectives.forEach((directive) => { if (!this.hasTrackBy(directive.expression)) { violations.push( this.createTrackByViolation( className, filePath, lineNumber, line, directive, ), ); } }); }); return violations; } /** * Extracts all ngFor directives from a template line. * @param line - The template line to analyze. * @returns Array of ngFor directive information. */ private extractNgForDirectives( line: string, ): { expression: string; startIndex: number }[] { const ngForRegex = /\*ngFor\s*=\s*["']([^"']*?)["']/g; const directives: { expression: string; startIndex: number }[] = []; for (const match of line.matchAll(ngForRegex)) { const expression = match[1]; if (expression) { directives.push({ expression, startIndex: match.index, }); } } return directives; } /** * Creates a violation for missing trackBy function. * @param className - The component class name. * @param filePath - The file path. * @param lineNumber - The line number. * @param line - The full line content. * @param directive - The ngFor directive information. * @returns The analysis result violation. */ private createTrackByViolation( className: string, filePath: string, lineNumber: number, line: string, directive: { expression: string; startIndex: number }, ): AnalysisResult { return { ruleId: this.id, message: `ngFor directive in '${className}' is missing trackBy function. This can cause performance issues with large lists.`, filePath, line: lineNumber, column: directive.startIndex, suggestedFix: `Add trackBy function: *ngFor="let item of items; trackBy: trackByFn" and implement trackByFn(item) { return item.id; }`, metadata: { className, violationType: 'missing-trackby', ngForExpression: directive.expression, templateSnippet: line.trim(), framework: 'angular', }, }; } /** * Checks if an ngFor expression contains a trackBy function. * @param ngForExpression - The ngFor expression to check. * @returns True if trackBy is present, false otherwise. */ private hasTrackBy(ngForExpression: string): boolean { // Check for trackBy in the ngFor expression return ( ngForExpression.includes('trackBy:') || ngForExpression.includes('trackBy :') ); } }