/** * @angular-modernizer/plugin-angular - Constructor Injection Transform Rule * * Transform rule that converts manual service instantiation (new Service()) * to inject(Service) calls, handles DIP violations, and validates by service suffixes. * * Philosophy: "Thin Rule, Thick Orchestrator" * - The rule is a minimal wrapper * - All business logic lives in the orchestrator * - The rule handles the plugin protocol (TransformRule interface) */ import type { TransformRule, TransformContext, TransformResult, } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import { ConstructorInjectionTransformOrchestrator } from '../orchestrators/constructor-injection-transform-orchestrator.js'; /** * Transform rule for converting manual service instantiation to inject() calls. * * @remarks * This rule implements the {@link TransformRule} interface and serves as the * bridge between the Angular Modernizer's plugin system and the constructor * injection transform orchestrator. It follows the "Thin Rule, Thick Orchestrator" pattern * where the rule handles protocol compliance while delegating all business logic * to the {@link ConstructorInjectionTransformOrchestrator}. * * ## What It Does * * The rule orchestrates the conversion of: * - **Manual instantiation**: Converts `new Service()` to `inject(Service)` * - **Import statements**: Adds `inject` import from `@angular/core` if not present * - **DIP violations**: Detects and corrects dependency inversion principle breaches * - **Service validation**: Validates classes by suffix patterns (Service, Repository, etc.) * * ## Example Usage * * This rule is typically not used directly, but rather accessed through the * {@link AngularPlugin}: * * ```typescript * const plugin = new AngularPlugin(); * const rules = plugin.getTransformRules(); * const rule = rules.find(r => r.id === 'angular:constructor-injection-transform'); * const result = await rule.transform(context); * ``` * * ## Transformation Behavior * * **Manual Instantiation**: * ```typescript * // Before * private dataService = new DataService(); * * // After * private dataService = inject(DataService); * ``` * * **DIP Violations**: * ```typescript * // Before (DIP violation) * constructor() { * this.service = new MyService(); * } * * // After (DIP compliant) * constructor() {} * private service = inject(MyService); * ``` * * @see {@link ConstructorInjectionTransformOrchestrator} for transformation logic * @see {@link TransformRule} for the interface contract * @see {@link AngularPlugin} for plugin integration * * @public */ export class ConstructorInjectionTransformRule implements TransformRule { /** * Unique rule identifier. * Format: "plugin-name:rule-name" */ public readonly id = 'angular:constructor-injection-transform'; /** * Human-readable rule name. */ public readonly name = 'Constructor Injection Transform'; /** * Rule description. */ public readonly description = 'Converts manual service instantiation (new Service() to inject(Service)) and fixes DIP violations'; /** * Rule category for grouping. */ public readonly category = 'modernization'; /** * Tags for filtering and search. */ public readonly tags = [ 'angular', 'standalone', 'dependency-injection', 'inject', 'dip', 'modernization', ]; /** * The orchestrator that performs the actual transformation. */ private readonly orchestrator = new ConstructorInjectionTransformOrchestrator(); /** * Transform a source file by converting manual service instantiation to inject() calls. * * @remarks * This method implements the {@link TransformRule.transform} contract. * It delegates to the {@link ConstructorInjectionTransformOrchestrator} for the actual * transformation work, then packages the result into a {@link TransformResult}. * * The method: * 1. Captures the file's initial state * 2. Invokes the orchestrator to perform the conversion * 3. Detects whether the file was modified * 4. Returns a standardized {@link TransformResult} * * ## Return Value * * The transform result includes: * - `modified`: Whether the file was changed * - `message`: A human-readable summary * - `filePath`: The path to the transformed file * - `ruleId`: This rule's identifier * * @param context - Transform context with file, project, and API access * * @returns Promise resolving to the transformation result * * @example * ```typescript * const rule = new ConstructorInjectionTransformRule(); * const result = await rule.transform(context); * * if (result.modified) { * console.info(`Converted manual instantiation: ${result.filePath}`); * } * ``` * * @public */ async transform( context: TransformContext, ): Promise { const { sourceFile, filePath } = context; // Check if this transformation type is requested const pluginConfig = context.config[ '@angular-modernizer/plugin-angular' ] as Record | undefined; if ( pluginConfig?.['transformationType'] !== 'constructor-injection-transform' ) { return { ruleId: this.id, modified: false, message: 'Transformation type not applicable', filePath, }; } // Capture initial state to detect modifications const initialText = sourceFile.getFullText(); // Run the orchestrator this.orchestrator.run(context); // Detect if the file was modified const modified = sourceFile.getFullText() !== initialText; // Generate appropriate message let message: string; if (modified) { // Count how many inject() calls were added const finalText = sourceFile.getFullText(); const injectCount = (finalText.match(/inject\(/g) ?? []).length; const initialInjectCount = (initialText.match(/inject\(/g) ?? []).length; const newInjectCount = injectCount - initialInjectCount; if (newInjectCount > 1) { message = `DIP violations corrected: converted ${newInjectCount} manual service instantiations to inject() calls`; } else { message = `Successfully converted manual service instantiation (new Service()) to inject(Service)`; } } else { message = `No manual service instantiations found to convert`; } // Return the result return { ruleId: this.id, modified, message, filePath, }; } }