/** * @angular-modernizer/plugin-angular - Service Without Injectable Rule * * Detects Angular services that are missing the @Injectable decorator. * Services must have @Injectable() to be properly registered with Angular's DI system. * * Detection Patterns: * - Classes with 'Service' in the name but no @Injectable decorator * - Classes that are injected as dependencies but lack @Injectable * - Services in provider arrays without proper decoration * * This rule ensures proper Angular dependency injection setup. */ import { SyntaxKind, type ClassDeclaration } from 'ts-morph'; import { ServiceDetector } from '@angular-modernizer/api'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; /** * Service Without Injectable Rule - detects services missing @Injectable decorator. */ export class ServiceWithoutInjectableRule implements AnalysisRule { public readonly id = 'angular:service-without-injectable'; public readonly name = 'Service Without Injectable'; public readonly description = 'Detects Angular services that are missing the @Injectable decorator'; public readonly severity = 'error'; public readonly category = 'angular-di'; public readonly tags = [ 'angular', 'dependency-injection', 'injectable', 'service', ]; async analyze(context: AnalysisContext): Promise { const violations: AnalysisResult[] = []; const sourceFile = context.sourceFile; // Use ServiceDetector for centralized service detection // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const recognizer = context.api.analysis.servicePatternRecognizer; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const detector = new ServiceDetector(recognizer); // Find all class declarations const classes = sourceFile.getDescendantsOfKind( SyntaxKind.ClassDeclaration, ); for (const classDecl of classes) { const violation = this.analyzeClass( classDecl, sourceFile.getFilePath(), detector, ); if (violation) { violations.push(violation); } } return violations; } private analyzeClass( classDecl: ClassDeclaration, filePath: string, detector: ServiceDetector, ): AnalysisResult | null { const className = classDecl.getName(); if (!className) { return null; } // Skip Angular components, directives, and pipes (they have different decorators) if (this.hasAngularComponentDecorators(classDecl)) { return null; } // Use ServiceDetector for centralized service detection const detectionResult = detector.detectService(classDecl); if (!detectionResult.isService) { return null; } // Check if it has @Injectable decorator if (this.hasInjectableDecorator(classDecl)) { return null; } // Check if it's used as a dependency (injected somewhere) const isInjected = this.isUsedAsDependency(classDecl, filePath); if (!isInjected) { return null; // Only flag if actually used as dependency } const startLine = classDecl.getStartLineNumber(); const startColumn = classDecl.getStart() - classDecl.getStartLinePos(); return { ruleId: this.id, message: `Service '${className}' is missing @Injectable decorator. Angular services must be decorated with @Injectable() to work with dependency injection.`, filePath, line: startLine, column: startColumn, suggestedFix: `Add @Injectable() decorator: @Injectable({ providedIn: 'root' }) export class ${className}`, metadata: { className, violationType: 'missing-injectable', isService: true, isInjected: true, framework: 'angular', detectionMethod: detectionResult.detectionMethod, confidence: detectionResult.confidence, }, }; } private hasAngularComponentDecorators(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); const componentDecorators = ['Component', 'Directive', 'Pipe']; return decorators.some((decorator) => componentDecorators.includes(decorator.getName()), ); } private hasInjectableDecorator(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((decorator) => decorator.getName() === 'Injectable'); } private isUsedAsDependency( classDecl: ClassDeclaration, _filePath: string, ): boolean { // This is a simplified check - in a real implementation, we'd need // to analyze the entire codebase to see if this class is injected anywhere // For now, we'll use heuristics const className = classDecl.getName() ?? ''; // Check if class has constructor parameters (suggests DI) const constructor = classDecl.getConstructors()[0]; if (constructor && constructor.getParameters().length > 0) { return true; } // Check for common service patterns return className.includes('Service') || className.includes('Provider'); } }