/** * @angular-modernizer/plugin-angular - Form Modernization Orchestrator * * The central "brain" of the form modernization plugin. * Orchestrates the modernization of Angular reactive forms to current best practices. * * Philosophy: "API-Driven Orchestration" * - Receives all capabilities via TransformContext * - Uses PublicApi for all AST operations * - Stateless - no constructor dependencies * - Pure business logic, no low-level manipulation */ import type { TransformContext } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import type { SourceFile, ClassDeclaration, CallExpression, PropertyAccessExpression, } from 'ts-morph'; import { SyntaxKind } from 'ts-morph'; /** * Configuration options for form modernization. */ export interface FormModernizationConfig { /** * Whether to convert FormBuilder usage to direct FormGroup/FormControl instantiation. * Default: true */ convertFormBuilder?: boolean; /** * Whether to modernize FormControl initialization (e.g., null to empty string). * Default: true */ modernizeFormControlInit?: boolean; /** * Whether to update validation patterns to current best practices. * Default: true */ updateValidationPatterns?: boolean; /** * Whether to convert ngOnInit form initialization to property initialization. * Default: true */ convertNgOnInitForms?: boolean; } /** * Form Modernization Orchestrator * * @remarks * The central orchestrator for modernizing Angular reactive forms. This class * transforms legacy reactive forms patterns to current Angular best practices. * * ## Architecture Pattern * * This orchestrator follows three core principles: * 1. **Stateless Design** - No constructor dependencies * 2. **Context-Driven** - All capabilities injected via {@link TransformContext} * 3. **API-First** - Uses {@link PublicApi} for all AST operations * * ## Modernization Workflow * * The orchestrator performs the following transformations: * 1. Detect FormBuilder usage in constructors and ngOnInit * 2. Convert FormBuilder.group() calls to new FormGroup() instantiation * 3. Convert FormBuilder.control() calls to new FormControl() instantiation * 4. Modernize FormControl initialization values * 5. Update validation patterns * 6. Convert ngOnInit form initialization to property initialization * 7. Update imports as needed * * ## Example Usage * * ```typescript * const orchestrator = new FormModernizationOrchestrator(); * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: { convertFormBuilder: true } * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link PublicApi} for available API tools * @see {@link FormModernizationConfig} for configuration options * * @public */ export class FormModernizationOrchestrator { /** * Creates a new FormModernizationOrchestrator instance. * * @remarks * The constructor is intentionally stateless. All dependencies are provided * through the {@link TransformContext} passed to the {@link run} method. * This enables testing, composition, and prevents tight coupling. * * @public */ constructor() { // Intentionally empty - follows the new architecture pattern } /** * Executes the form modernization on a single source file. * * @remarks * This method is the main entry point for the orchestrator. It receives all * necessary dependencies (project, API, config) through the context parameter, * scans the file for reactive forms patterns, and modernizes them. * * The method operates on one source file at a time and is safe to call * multiple times. It will: * - Detect FormBuilder usage and convert to direct instantiation * - Modernize FormControl initialization * - Update validation patterns * - Convert ngOnInit form setup to property initialization * * ## Modernization Behavior * * **FormBuilder Conversion**: * - `this.fb.group({...})` → `new FormGroup({...})` * - `this.fb.control(...)` → `new FormControl(...)` * - `this.fb.array(...)` → `new FormArray(...)` * * **FormControl Initialization**: * - `new FormControl(null)` → `new FormControl('')` * - `new FormControl(undefined)` → `new FormControl('')` * * **Validation Patterns**: * - Updates deprecated validation syntax * - Ensures proper validator composition * * @param context - The transformation context containing: * - `sourceFile`: The TypeScript source file to modernize * - `project`: The ts-morph Project for cross-file analysis * - `api`: The PublicApi with analysis and transformation tools * - `config`: User-provided configuration options * * @example * ```typescript * // Basic usage * orchestrator.run(context); * * // With custom configuration * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: { * convertFormBuilder: false, // Skip FormBuilder conversion * modernizeFormControlInit: true, * } * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link FormModernizationConfig} for configuration options * * @public */ run(context: TransformContext): void { const { sourceFile, config } = context; // Get configuration with defaults const modernizationConfig = this.getModernizationConfig(config); // Find all classes in the file const classes = sourceFile.getClasses(); for (const classDecl of classes) { // Check if this class uses reactive forms if (this.usesReactiveForms(classDecl, sourceFile)) { this.modernizeClassForms(classDecl, sourceFile, modernizationConfig); } } // Update imports if needed this.updateImports(sourceFile, modernizationConfig); } /** * Modernize forms in a single class. */ private modernizeClassForms( classDecl: ClassDeclaration, sourceFile: SourceFile, config: FormModernizationConfig, ): void { // Convert FormBuilder usage if (config.convertFormBuilder) { this.convertFormBuilderUsage(classDecl, sourceFile); } // Modernize FormControl initialization if (config.modernizeFormControlInit) { this.modernizeFormControlInitialization(classDecl, sourceFile); } // Update validation patterns if (config.updateValidationPatterns) { this.updateValidationPatterns(classDecl, sourceFile); } // Convert ngOnInit form initialization if (config.convertNgOnInitForms) { this.convertNgOnInitFormInitialization(classDecl, sourceFile); } } /** * Check if a class uses reactive forms. */ private usesReactiveForms( classDecl: ClassDeclaration, sourceFile: SourceFile, ): boolean { // Check for FormBuilder in constructor parameters const constructor = classDecl.getConstructors()[0]; if (constructor) { const parameters = constructor.getParameters(); const hasFbParam = parameters.some((param) => param.getType().getText().includes('FormBuilder'), ); if (hasFbParam) { return true; } } // Check for FormBuilder properties (by name or initializer) const classProperties = classDecl.getProperties(); const hasFbProperty = classProperties.some((prop) => { const propName = prop.getName(); const typeText = prop.getType().getText(); const initializer = prop.getInitializer(); // Check by type if (typeText.includes('FormBuilder')) { return true; } // Check by name (common patterns) if (propName === 'fb' || propName === 'formBuilder') { return true; } // Check by initializer if (initializer?.getText().includes('FormBuilder')) { return true; } return false; }); if (hasFbProperty) { return true; } // Check for FormGroup/FormControl imports const imports = sourceFile.getImportDeclarations(); const hasFormsImports = imports.some((imp) => imp.getModuleSpecifierValue().includes('@angular/forms'), ); if (!hasFormsImports) { return false; } // Check for form-related properties or methods const hasFormProperties = classProperties.some((prop) => { const propName = prop.getName(); const typeText = prop.getType().getText(); const initializer = prop.getInitializer(); // Check by type if ( typeText.includes('FormGroup') || typeText.includes('FormControl') || typeText.includes('FormArray') ) { return true; } // Check by name (common patterns) if ( propName.includes('Form') || propName.includes('Control') || propName === 'form' ) { return true; } // Check by initializer if ( initializer?.getText().includes('FormGroup') || initializer?.getText().includes('FormControl') || initializer?.getText().includes('FormArray') || initializer?.getText().includes('fb.group') || initializer?.getText().includes('fb.control') ) { return true; } return false; }); return hasFormProperties; } /** * Convert FormBuilder usage to direct instantiation. */ private convertFormBuilderUsage( classDecl: ClassDeclaration, sourceFile: SourceFile, ): void { // Find FormBuilder property const fbProperty = classDecl.getProperty('fb'); if (!fbProperty) { return; } // Find all usages of this.fb.group(), this.fb.control(), etc. const methods = classDecl.getMethods(); const constructor = classDecl.getConstructors()[0]; const properties = classDecl.getProperties(); // Check constructor, methods, and property initializers for FormBuilder usage const nodesToCheck = [constructor, ...methods, ...properties].filter( Boolean, ); for (const node of nodesToCheck) { if (!node) { continue; } // Find property access expressions like this.fb.group const propertyAccesses = node.getDescendantsOfKind( SyntaxKind.PropertyAccessExpression, ); const fbAccesses = propertyAccesses.filter( (access) => access.getExpression().getText() === 'this' && access.getName() === 'fb', ); for (const fbAccess of fbAccesses) { const parent = fbAccess.getParent(); let callExpr: CallExpression | undefined; // Check if parent is a property access (like this.fb.group) if ( parent && parent.getKind() === SyntaxKind.PropertyAccessExpression ) { const grandParent = parent.getParent(); if ( grandParent && grandParent.getKind() === SyntaxKind.CallExpression ) { callExpr = grandParent as CallExpression; } } else if (parent && parent.getKind() === SyntaxKind.CallExpression) { callExpr = parent as CallExpression; } if (callExpr) { // Get the method name from the property access expression that's the direct child of the call let methodName = ''; if ( parent && parent.getKind() === SyntaxKind.PropertyAccessExpression ) { const parentPropAccess = parent as PropertyAccessExpression; methodName = parentPropAccess.getName(); } if (methodName === 'group') { this.convertFormBuilderGroup(callExpr); } else if (methodName === 'control') { this.convertFormBuilderControl(callExpr); } else if (methodName === 'array') { this.convertFormBuilderArray(callExpr); } } } } // Remove FormBuilder from constructor if no longer used this.removeUnusedFormBuilder(classDecl, sourceFile); } /** * Convert FormBuilder.group() to new FormGroup(). */ private convertFormBuilderGroup(callExpr: CallExpression): void { const args = callExpr.getArguments(); if (args.length > 0 && args[0]) { const controlsArg = args[0]; callExpr.replaceWithText(`new FormGroup(${controlsArg.getText()})`); } } /** * Convert FormBuilder.control() to new FormControl(). */ private convertFormBuilderControl(callExpr: CallExpression): void { const args = callExpr.getArguments(); const argsText = args.map((arg) => arg.getText()).join(', '); callExpr.replaceWithText(`new FormControl(${argsText})`); } /** * Convert FormBuilder.array() to new FormArray(). */ private convertFormBuilderArray(callExpr: CallExpression): void { const args = callExpr.getArguments(); if (args.length > 0 && args[0]) { const controlsArg = args[0]; callExpr.replaceWithText(`new FormArray(${controlsArg.getText()})`); } } /** * Remove unused FormBuilder from constructor. */ private removeUnusedFormBuilder( classDecl: ClassDeclaration, _sourceFile: SourceFile, ): void { const constructor = classDecl.getConstructors()[0]; if (!constructor) { return; } const parameters = constructor.getParameters(); const fbParam = parameters.find((param) => param.getType().getText().includes('FormBuilder'), ); if (fbParam) { // Check if FormBuilder is still used elsewhere const fbUsages = classDecl .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) .filter( (access) => access.getExpression().getText() === 'this' && access.getName() === 'fb', ); if (fbUsages.length === 0) { // Remove FormBuilder parameter fbParam.remove(); // Remove FormBuilder property if it exists const fbProperty = classDecl.getProperty('fb'); if (fbProperty) { fbProperty.remove(); } // Clean up constructor if empty if (constructor.getParameters().length === 0) { constructor.remove(); } } } } /** * Modernize FormControl initialization values. */ private modernizeFormControlInitialization( _classDecl: ClassDeclaration, sourceFile: SourceFile, ): void { // Find all new FormControl() calls const newExpressions = sourceFile.getDescendantsOfKind( SyntaxKind.NewExpression, ); const formControlNews = newExpressions.filter( (expr) => expr.getExpression().getText() === 'FormControl', ); for (const newExpr of formControlNews) { const args = newExpr.getArguments(); if (args.length > 0) { const firstArg = args[0]; if (firstArg) { const firstArgText = firstArg.getText(); // Convert null/undefined to empty string if (firstArgText === 'null' || firstArgText === 'undefined') { firstArg.replaceWithText("''"); } } } } } /** * Update validation patterns to current best practices. */ private updateValidationPatterns( _classDecl: ClassDeclaration, _sourceFile: SourceFile, ): void { // TODO: This is a placeholder for validation pattern updates // Could include things like: // - Converting old validator syntax // - Ensuring proper validator composition // - Updating deprecated validators // For now, we'll focus on the main FormBuilder conversion // Validation updates can be added in future iterations } /** * Convert ngOnInit form initialization to property initialization. */ private convertNgOnInitFormInitialization( _classDecl: ClassDeclaration, _sourceFile: SourceFile, ): void { // TODO: Implement - convert ngOnInit form initialization to property initialization // for better performance and cleaner code. This involves: // - Detecting form initialization in ngOnInit methods // - Moving FormGroup/FormControl creation to property declarations // - Removing the ngOnInit method if it becomes empty // - Handling complex initialization logic that depends on @Input properties // This is a complex transformation that requires careful AST analysis // For now, we'll focus on the core FormBuilder conversion } /** * Update imports based on modernization changes. */ private updateImports( sourceFile: SourceFile, config: FormModernizationConfig, ): void { const importManager = { addNamedImport: (module: string, name: string) => { const existingImport = sourceFile.getImportDeclaration(module); if (existingImport) { const namedImports = existingImport.getNamedImports(); const hasImport = namedImports.some((imp) => imp.getName() === name); if (!hasImport) { existingImport.addNamedImport(name); } } else { sourceFile.addImportDeclaration({ moduleSpecifier: module, namedImports: [name], }); } }, removeNamedImport: (module: string, name: string) => { const existingImport = sourceFile.getImportDeclaration(module); if (existingImport) { const namedImport = existingImport .getNamedImports() .find((imp) => imp.getName() === name); if (namedImport) { namedImport.remove(); } } }, }; if (config.convertFormBuilder) { // Add FormGroup, FormControl, FormArray imports if needed const hasFormGroupUsage = sourceFile.getText().includes('new FormGroup'); const hasFormControlUsage = sourceFile .getText() .includes('new FormControl'); const hasFormArrayUsage = sourceFile.getText().includes('new FormArray'); if (hasFormGroupUsage) { importManager.addNamedImport('@angular/forms', 'FormGroup'); } if (hasFormControlUsage) { importManager.addNamedImport('@angular/forms', 'FormControl'); } if (hasFormArrayUsage) { importManager.addNamedImport('@angular/forms', 'FormArray'); } // Remove FormBuilder import if no longer used const hasFormBuilderUsage = sourceFile.getText().includes('FormBuilder'); if (!hasFormBuilderUsage) { importManager.removeNamedImport('@angular/forms', 'FormBuilder'); } } } /** * Get modernization configuration with defaults. */ private getModernizationConfig( config: Record, ): FormModernizationConfig { return { convertFormBuilder: config['convertFormBuilder'] !== false, modernizeFormControlInit: config['modernizeFormControlInit'] !== false, updateValidationPatterns: config['updateValidationPatterns'] !== false, convertNgOnInitForms: config['convertNgOnInitForms'] !== false, }; } }