/** * @angular-modernizer/plugin-angular - Inheritance to Composition Transform Rule * * Transform rule that refactors inheritance patterns to composition using dependency injection. * This rule wraps the InheritanceAnalyzer and InheritanceToCompositionTransformer from the core package. */ import type { TransformRule, TransformContext, TransformResult, } from '@angular-modernizer/plugin-system'; import { InheritanceAnalyzer, InheritanceToCompositionTransformer, type InheritanceToCompositionConfig, } from '@angular-modernizer/core'; /** * Transform rule for refactoring inheritance to composition. * * @remarks * This rule detects classes that extend base classes and refactors them to use * composition via dependency injection instead. It extracts base class methods * into separate services and updates the derived class to inject those services. * * ## Pattern Detection * - Detects classes with `extends` clauses * - Analyzes method usage and extractability * - Calculates confidence scores based on complexity * * ## Transformation Strategy * 1. Analyze inheritance patterns in the source file * 2. Filter patterns by confidence threshold * 3. For each high-confidence pattern: * - Extract base class methods into a new service * - Remove the extends clause from the derived class * - Add constructor parameter for the extracted service * - Update method calls to use the injected service * - Add Angular dependency injection decorators * * ## Configuration Options * - `minConfidence`: Minimum confidence score (0-1) to perform transformation (default: 0.8) * - `requireApproval`: Require user approval for medium-confidence transformations (default: false) * - `safeMode`: Generate only high-confidence transformations (default: false) * - `preserveOriginalAsComments`: Preserve original code as comments (default: false) * - `useInjectFunction`: Use modern inject() function instead of constructor injection (default: true) * * @example * ```typescript * // Before transformation * export class CustomDateAdapter extends NativeDateAdapter { * parse(value: any): Date | null { ... } * format(date: Date, displayFormat: any): string { ... } * } * * // After transformation (with extract strategy) * @Injectable() * export class DateFormattingService { * parse(value: any): Date | null { ... } * format(date: Date, displayFormat: any): string { ... } * } * * @Injectable() * export class CustomDateAdapter { * constructor( * private dateFormatting: DateFormattingService, * private nativeDateAdapter: NativeDateAdapter * ) {} * } * ``` */ export class InheritanceToCompositionRule implements TransformRule { /** * Unique rule identifier. */ readonly id = 'angular:transform:inheritance-to-composition'; /** * Human-readable rule name. */ readonly name = 'Inheritance to Composition'; /** * Rule description. */ readonly description = 'Refactor inheritance patterns to composition using dependency injection'; /** * Rule category for grouping. */ readonly category = 'refactoring'; /** * Tags for filtering and search. */ readonly tags = [ 'inheritance', 'composition', 'dependency-injection', 'solid', 'architecture', ]; /** * Transform a source file by refactoring inheritance to composition. * * @param context - Transformation context with source file and configuration * * @returns Transformation result with success status and change details */ async transform(context: TransformContext): Promise { const sourceFile = context.sourceFile; const project = context.project; const filePath = sourceFile.getFilePath(); // Filter by transformation type if specified const transformationType = context.config?.['transformationType'] as | string | undefined; if ( transformationType && transformationType !== 'inheritance-to-composition' ) { return { ruleId: this.id, modified: false, filePath, message: `Skipped: transformation type '${transformationType}' does not match 'inheritance-to-composition'`, }; } // Check if file has any inheritance patterns const classes = sourceFile.getClasses(); const classesWithInheritance = classes.filter( (cls) => cls.getExtends() !== undefined, ); if (classesWithInheritance.length === 0) { return { ruleId: this.id, modified: false, filePath, message: 'No inheritance patterns found', }; } // Create analyzer and transformer configuration const minConfidence = (context.config?.['minConfidence'] as number | undefined) ?? 0.8; const transformerConfig: InheritanceToCompositionConfig = { minConfidence, requireApproval: (context.config?.['requireApproval'] as boolean | undefined) ?? false, safeMode: (context.config?.['safeMode'] as boolean | undefined) ?? false, preserveOriginalAsComments: (context.config?.['preserveOriginalAsComments'] as | boolean | undefined) ?? false, useInjectFunction: (context.config?.['useInjectFunction'] as boolean | undefined) ?? true, }; // Create analyzer and transformer instances const analyzer = new InheritanceAnalyzer(); const transformer = new InheritanceToCompositionTransformer( transformerConfig, ); try { // Analyze inheritance patterns across the project const analysisResult = await analyzer.analyze(project); // Filter patterns to only those in the current source file const patternsInFile = analysisResult.patterns.filter( (p) => p.derivedClassPath === filePath, ); if (patternsInFile.length === 0) { return { ruleId: this.id, modified: false, filePath, message: 'No transformable inheritance patterns found in this file', }; } // Filter patterns by confidence const transformablePatterns = patternsInFile.filter( (p) => p.overallConfidence >= minConfidence, ); if (transformablePatterns.length === 0) { return { ruleId: this.id, modified: false, filePath, message: `No patterns meet minimum confidence threshold (${minConfidence})`, }; } // Transform each pattern let totalChanges = 0; const transformResults: { success: boolean; className: string; summary: string; }[] = []; for (const pattern of transformablePatterns) { const result = await transformer.transformPattern(pattern, project); transformResults.push({ success: result.success, className: pattern.derivedClassName, summary: result.summary, }); if (result.success) { // Count total methods extracted totalChanges += result.totalMethodsExtracted; } } const successfulTransforms = transformResults.filter( (r) => r.success, ).length; const failedTransforms = transformResults.filter( (r) => !r.success, ).length; if (successfulTransforms === 0) { return { ruleId: this.id, modified: false, filePath, message: `All transformations failed (${failedTransforms} attempted)`, }; } let message = `Successfully refactored ${successfulTransforms} class(es) from inheritance to composition`; if (failedTransforms > 0) { message += ` (${failedTransforms} failed)`; } return { ruleId: this.id, modified: true, filePath, message, changeCount: totalChanges, }; } catch (error) { return { ruleId: this.id, modified: false, filePath, message: `Transformation failed: ${error instanceof Error ? error.message : String(error)}`, }; } } }