/** * @angular-modernizer/plugin-angular - Interface Extraction Orchestrator * * The central "brain" of the interface extraction transformation plugin. * Orchestrates the extraction of inline interface types to named interfaces. * * 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, MethodDeclaration, ParameterDeclaration, PropertyDeclaration, TypeLiteralNode, TypeNode, } from 'ts-morph'; import { SyntaxKind } from 'ts-morph'; /** * Interface Extraction Orchestrator * * @remarks * The central orchestrator for extracting inline interface types to named interfaces. * This class demonstrates the "API-Driven Plugin" pattern where all capabilities are * received via context objects rather than constructor injection. * * ## 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 * * ## Transformation Workflow * * The orchestrator performs the following steps: * 1. Find classes with inline interface types in constructors, methods, and properties * 2. Extract inline type literals to named interfaces * 3. Update type references to use the new interface names * 4. Ensure proper naming and avoid conflicts * * ## Example Usage * * ```typescript * const orchestrator = new InterfaceExtractionOrchestrator(); * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: {} * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link PublicApi} for available API tools * * @public */ export class InterfaceExtractionOrchestrator { /** * Creates a new InterfaceExtractionOrchestrator 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 interface extraction transformation 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 classes with inline interface types, and extracts them * to named interfaces. * * The method operates on one source file at a time and is safe to call * multiple times. It will: * - Find all classes in the file * - Extract inline types from constructors, methods, and properties * - Create named interfaces with appropriate names * - Update type references to use the new interfaces * * ## Transformation Behavior * * **Constructor Parameters**: * ```typescript * // Before * constructor(private config: { apiUrl: string; timeout: number }) {} * * // After * interface Config { apiUrl: string; timeout: number; } * constructor(private config: Config) {} * ``` * * **Method Returns**: * ```typescript * // Before * getUser(): { name: string; email: string } { ... } * * // After * interface User { name: string; email: string; } * getUser(): User { ... } * ``` * * @param context - The transformation context containing: * - `sourceFile`: The TypeScript source file to transform * - `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: { interfaceNamingStrategy: 'PascalCase' } * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * * @public */ run(context: TransformContext): void { const { sourceFile } = context; // Find all classes in the file const classes = sourceFile.getClasses(); for (const classDecl of classes) { this.extractInterfacesFromClass(classDecl, sourceFile); } } /** * Extract interfaces from a single class. * * @param classDecl - The class declaration to process * @param sourceFile - The source file containing the class */ private extractInterfacesFromClass( classDecl: ClassDeclaration, sourceFile: SourceFile, ): void { // Extract from constructor parameters const constructor = classDecl.getConstructors()[0]; if (constructor) { this.extractFromParameters(constructor.getParameters(), sourceFile); } // Extract from methods for (const method of classDecl.getMethods()) { this.extractFromMethod(method, sourceFile); } // Extract from properties for (const property of classDecl.getProperties()) { this.extractFromProperty(property, sourceFile); } } /** * Extract interfaces from constructor/method parameters. * * @param parameters - The parameters to process * @param sourceFile - The source file */ private extractFromParameters( parameters: ParameterDeclaration[], sourceFile: SourceFile, ): void { for (const param of parameters) { const typeNode = param.getTypeNode(); if (typeNode && this.isExtractableTypeLiteral(typeNode)) { this.extractInterfaceFromTypeLiteral(typeNode, sourceFile); } } } /** * Extract interfaces from a method's parameters and return type. * * @param method - The method to process * @param sourceFile - The source file */ private extractFromMethod( method: MethodDeclaration, sourceFile: SourceFile, ): void { // Extract from parameters this.extractFromParameters(method.getParameters(), sourceFile); // Extract from return type const returnTypeNode = method.getReturnTypeNode(); if (returnTypeNode && this.isExtractableTypeLiteral(returnTypeNode)) { this.extractInterfaceFromTypeLiteral(returnTypeNode, sourceFile); } } /** * Extract interfaces from a property's type. * * @param property - The property to process * @param sourceFile - The source file */ private extractFromProperty( property: PropertyDeclaration, sourceFile: SourceFile, ): void { const typeNode = property.getTypeNode(); if (typeNode && this.isExtractableTypeLiteral(typeNode)) { this.extractInterfaceFromTypeLiteral(typeNode, sourceFile); } } /** * Check if a type node is an extractable type literal. * * @param typeNode - The type node to check * @returns True if the type can be extracted to an interface */ private isExtractableTypeLiteral( typeNode: TypeNode | undefined, ): typeNode is TypeLiteralNode { if (!typeNode) { return false; } return typeNode.getKind() === SyntaxKind.TypeLiteral; } /** * Extract an interface from a type literal node. * * @param typeLiteral - The type literal to extract * @param sourceFile - The source file */ private extractInterfaceFromTypeLiteral( typeLiteral: TypeLiteralNode, sourceFile: SourceFile, ): void { // Skip if no properties const properties = typeLiteral.getProperties(); if (properties.length === 0) { return; } // Generate a unique interface name const interfaceName = this.generateInterfaceName(sourceFile); // Create the interface declaration sourceFile.addInterface({ name: interfaceName, properties: properties.map((prop) => ({ name: prop.getName(), type: prop.getTypeNode()?.getText() ?? 'any', hasQuestionToken: prop.hasQuestionToken(), })), }); // Replace the type literal with the interface name typeLiteral.replaceWithText(interfaceName); } /** * Generate a unique interface name for the source file. * * @param sourceFile - The source file * @returns A unique interface name */ private generateInterfaceName(sourceFile: SourceFile): string { const existingInterfaces = sourceFile .getInterfaces() .map((i) => i.getName()); let counter = 1; let name = 'ExtractedInterface'; while ( existingInterfaces.includes(name) || sourceFile.getText().includes(name) ) { name = `ExtractedInterface${counter}`; counter++; } return name; } }