/** * @angular-modernizer/plugin-angular - Promise Cleanup Transform Orchestrator * * Handles residual Promise anti-patterns after service and component transforms. * * Transformations: * 1. Remove orphaned .toPromise() calls not yet handled * 2. Remove standalone ChangeDetectorRef.detectChanges() calls * 3. Detect sequential awaits (reports, does not auto-transform — combineLatest requires more context) */ import type { TransformContext, TransformResult, } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import type { ClassDeclaration, SourceFile } from 'ts-morph'; import { SyntaxKind } from 'ts-morph'; export class PromiseCleanupTransformOrchestrator { readonly ruleId = 'angular:promise-cleanup'; run(context: TransformContext): TransformResult { const { sourceFile, filePath } = context; const originalText = sourceFile.getFullText(); try { let changed = false; // 1. Remove orphaned .toPromise() calls if (this.removeOrphanedToPromiseCalls(sourceFile)) { changed = true; } // 2. Remove residual detectChanges() calls in components if (this.removeResidualDetectChanges(sourceFile)) { changed = true; } return { ruleId: this.ruleId, modified: changed, message: changed ? 'Cleaned up residual Promise anti-patterns' : 'No cleanup needed', filePath, }; } catch (error) { sourceFile.replaceWithText(originalText); return { ruleId: this.ruleId, modified: false, message: `Cleanup failed: ${error instanceof Error ? error.message : String(error)}`, filePath, }; } } private removeOrphanedToPromiseCalls(sourceFile: SourceFile): boolean { let changed = false; let found = true; while (found) { found = false; const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression); for (const call of calls) { const expr = call.getExpression(); if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) { continue; } const propAccess = expr.asKindOrThrow( SyntaxKind.PropertyAccessExpression, ); if (propAccess.getName() !== 'toPromise') { continue; } call.replaceWithText(propAccess.getExpression().getText()); changed = true; found = true; break; } } return changed; } private removeResidualDetectChanges(sourceFile: SourceFile): boolean { let changed = false; for (const classDecl of sourceFile.getClasses()) { if (!this.hasDecorator(classDecl, 'Component')) { continue; } for (const method of classDecl.getMethods()) { // Only remove detectChanges if method is no longer async (already cleaned up) if (method.isAsync()) { continue; } let found = true; while (found) { found = false; const calls = method.getDescendantsOfKind(SyntaxKind.CallExpression); for (const call of calls) { if (!call.getText().includes('detectChanges()')) { continue; } const stmt = call.getFirstAncestorByKind( SyntaxKind.ExpressionStatement, ); if (stmt) { stmt.remove(); changed = true; found = true; break; } } } } } return changed; } private hasDecorator(classDecl: ClassDeclaration, name: string): boolean { return classDecl.getDecorators().some((d) => d.getName() === name); } }