/* 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 - Directive Without Selector Rule * * Detects Angular directives that are missing the selector property in their @Directive decorator. * Directives must have a selector to be applied to elements. * * Detection Patterns: * - @Directive decorators without selector property * - Directives with empty or invalid selectors * * This rule ensures directives can be properly applied to DOM elements. */ import { SyntaxKind, type ClassDeclaration, type Decorator, type PropertyAssignment, } from 'ts-morph'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; /** * Directive Without Selector Rule - detects directives missing selector property. */ export class DirectiveWithoutSelectorRule implements AnalysisRule { public readonly id = 'angular:directive-without-selector'; public readonly name = 'Directive Without Selector'; public readonly description = 'Detects Angular directives that are missing the selector property'; public readonly severity = 'error'; public readonly category = 'angular-directive'; public readonly tags = ['angular', 'directive', 'selector', '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 directive const directiveDecorator = this.getDirectiveDecorator(classDecl); if (!directiveDecorator) { return null; } // Check if it has a selector const hasValidSelector = this.hasValidSelector(directiveDecorator); if (hasValidSelector) { return null; } const startLine = classDecl.getStartLineNumber(); const startColumn = classDecl.getStart() - classDecl.getStartLinePos(); return { ruleId: this.id, message: `Directive '${className}' is missing a selector property. Angular directives must have a selector to be applied to elements.`, filePath, line: startLine, column: startColumn, suggestedFix: `Add selector property: @Directive({ selector: '[app${className.replace('Directive', '')}]', ... })`, metadata: { className, violationType: 'missing-selector', isDirective: true, framework: 'angular', }, }; } private getDirectiveDecorator( classDecl: ClassDeclaration, ): Decorator | undefined { const decorators = classDecl.getDecorators(); return decorators.find((decorator) => decorator.getName() === 'Directive'); } private hasValidSelector(directiveDecorator: Decorator): boolean { const args = directiveDecorator.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 selectorProperty = properties.find((prop: any) => { if (prop.getKind() === SyntaxKind.PropertyAssignment) { const name = prop.getNameNode(); return name.getText() === 'selector'; } return false; }) as PropertyAssignment | undefined; if (!selectorProperty) { return false; } // Check if selector has a non-empty value const initializer = selectorProperty.getInitializer(); if (!initializer) { return false; } const selectorValue = initializer.getText().replaceAll(/['"]/g, ''); return selectorValue.trim().length > 0; } }