/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /** * @angular-modernizer/plugin-angular - Pipe Without Name Rule * * Detects Angular pipes that are missing the name property in their @Pipe decorator. * Pipes must have a name to be used in templates. * * Detection Patterns: * - @Pipe decorators without name property * - Pipes with empty or invalid names * * This rule ensures pipes can be properly referenced in templates. */ import { SyntaxKind, type ClassDeclaration, type Decorator, type PropertyAssignment, } from 'ts-morph'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; /** * Pipe Without Name Rule - detects pipes missing name property. */ export class PipeWithoutNameRule implements AnalysisRule { public readonly id = 'angular:pipe-without-name'; public readonly name = 'Pipe Without Name'; public readonly description = 'Detects Angular pipes that are missing the name property'; public readonly severity = 'error'; public readonly category = 'angular-pipe'; public readonly tags = ['angular', 'pipe', 'name', 'template']; 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.analyzeClass(classDecl, sourceFile.getFilePath()); if (violation) { violations.push(violation); } } return violations; } private analyzeClass( classDecl: ClassDeclaration, filePath: string, ): AnalysisResult | null { const className = classDecl.getName(); if (!className) { return null; } // Check if this is a pipe const pipeDecorator = this.getPipeDecorator(classDecl); if (!pipeDecorator) { return null; } // Check if it has a name const hasValidName = this.hasValidName(pipeDecorator); if (hasValidName) { return null; } const startLine = classDecl.getStartLineNumber(); const startColumn = classDecl.getStart() - classDecl.getStartLinePos(); return { ruleId: this.id, message: `Pipe '${className}' is missing a name property. Angular pipes must have a name to be used in templates.`, filePath, line: startLine, column: startColumn, suggestedFix: `Add name property: @Pipe({ name: '${className.replace('Pipe', '').toLowerCase()}', ... })`, metadata: { className, violationType: 'missing-name', isPipe: true, framework: 'angular', }, }; } private getPipeDecorator(classDecl: ClassDeclaration): Decorator | undefined { const decorators = classDecl.getDecorators(); return decorators.find((decorator) => decorator.getName() === 'Pipe'); } private hasValidName(pipeDecorator: Decorator): boolean { const args = pipeDecorator.getArguments(); if (args.length === 0) { return false; } const config = args[0]; if (!config || config.getKind() !== SyntaxKind.ObjectLiteralExpression) { return false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const configObj = config as any; // Type assertion for ObjectLiteralExpression const properties = configObj.getProperties(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const nameProperty = properties.find((prop: any) => { if (prop.getKind() === SyntaxKind.PropertyAssignment) { const name = prop.getNameNode(); return name.getText() === 'name'; } return false; }) as PropertyAssignment | undefined; if (!nameProperty) { return false; } // Check if name has a non-empty value const initializer = nameProperty.getInitializer(); if (!initializer) { return false; } const nameValue = initializer.getText().replaceAll(/['"]/g, ''); return nameValue.trim().length > 0; } }