/** * @angular-modernizer/plugin-angular - Constructor Dependency Helper * * Helper for extracting constructor dependencies from service classes. * Works WITH DependencyAnalyzer, not replacing it. * * Philosophy: "Extract What's Actually Used" * - Analyzes constructor parameters * - Filters to only dependencies used by specific methods * - Provides type information for import resolution * * Use Case: Service Bag Transform * - Extract dependencies needed by focused services * - Ensure focused services get only what they need * - Support import path resolution via SymbolLocator */ import type { ClassDeclaration, MethodDeclaration, ConstructorDeclaration, ParameterDeclaration, } from 'ts-morph'; import { Scope } from 'ts-morph'; /** * Represents a constructor dependency with all metadata needed for injection. */ export interface ConstructorDependency { /** * Parameter name (e.g., 'http', 'logger'). */ name: string; /** * Type of the parameter (e.g., 'HttpClient', 'LoggerService'). */ type: string; /** * Scope of the parameter (Private, Public, Protected). */ scope: Scope; /** * Whether the parameter is readonly. */ isReadonly: boolean; /** * Original parameter declaration for advanced analysis. */ parameterDeclaration?: ParameterDeclaration; } /** * Constructor Dependency Helper - extracts dependencies used by specific methods. * * This helper works WITH the existing DependencyAnalyzer from @angular-modernizer/api. * It focuses on constructor-level dependencies, while DependencyAnalyzer handles * import-level dependencies. * * @example * ```typescript * const helper = new ConstructorDependencyHelper(); * * // Extract dependencies used by data-retrieval methods * const dependencies = helper.extractUsedDependencies( * serviceClass, * dataRetrievalMethods * ); * * // Result: [{ name: 'http', type: 'HttpClient', scope: Private, isReadonly: true }] * ``` */ export class ConstructorDependencyHelper { /** * Extracts constructor dependencies that are actually used by the provided methods. * * This is the primary method for Service Bag transformation: * 1. Extracts all constructor parameters from the class * 2. Filters to only those used in the provided methods (via `this.xyz` references) * 3. Returns dependencies with full metadata for constructor generation * * @param classDecl - Service class declaration * @param methods - Methods that will use these dependencies (e.g., focused service methods) * @returns Array of dependencies actually used by the methods * * @example * ```typescript * // Service has: constructor(private http: HttpClient, private logger: LoggerService) * // Methods use: this.http.get(), this.http.post() * // Result: [{ name: 'http', type: 'HttpClient', ... }] * // logger is NOT included because methods don't use it * ``` */ extractUsedDependencies( classDecl: ClassDeclaration, methods: MethodDeclaration[], ): ConstructorDependency[] { // Get constructor const constructor = classDecl.getConstructors()[0]; if (!constructor) { return []; // No constructor = no dependencies } // Extract all constructor parameters const allDependencies = this.extractConstructorParameters(constructor); // Filter to only those used in methods return this.filterUsedInMethods(allDependencies, methods); } /** * Extracts all constructor parameters with full metadata. * * Handles: * - Access modifiers (private, public, protected) * - Readonly modifier * - Type annotations * - Parameter names * * @param constructor - Constructor declaration * @returns Array of all constructor dependencies * @private */ private extractConstructorParameters( constructor: ConstructorDeclaration, ): ConstructorDependency[] { return constructor.getParameters().map((param) => { // Use the declared type node text to avoid fully-qualified paths in in-memory projects const type = param.getTypeNode()?.getText() ?? param.getType().getText(); // Get scope (default to Private if not specified) const scope = param.getScope() ?? Scope.Private; // Check if readonly const isReadonly = param.isReadonly(); return { name: param.getName(), type, scope, isReadonly, parameterDeclaration: param, }; }); } /** * Filters dependencies to only those used in the provided methods. * * Detection Strategy: * - Searches for `this.` patterns in method bodies * - Uses regex for performance (AST traversal too slow for this use case) * - Handles common patterns: * - Property access: `this.http.get()` * - Method calls: `this.logger.log()` * - Nested access: `this.http.post().pipe()` * * Edge Cases: * - Handles `this.http` in comments (false positive, acceptable) * - Handles `this.httpService` vs `this.http` (word boundary check) * - Does NOT detect indirect usage (e.g., `const h = this.http; h.get()`) * * @param dependencies - All constructor dependencies * @param methods - Methods to analyze for usage * @returns Filtered dependencies actually used in methods * @private */ private filterUsedInMethods( dependencies: ConstructorDependency[], methods: MethodDeclaration[], ): ConstructorDependency[] { const usedNames = new Set(); // Analyze each method body for (const method of methods) { const body = method.getBodyText() ?? ''; // Check each dependency for (const dep of dependencies) { // Pattern: `this.` with word boundary // Word boundary ensures we don't match `this.httpService` when looking for `this.http` const pattern = new RegExp( String.raw`this\.${this.escapeRegex(dep.name)}\b`, 'g', ); if (pattern.test(body)) { usedNames.add(dep.name); } } } return dependencies.filter((dep) => usedNames.has(dep.name)); } /** * Escapes special regex characters in a string. * * Handles dependency names that might contain special characters. * Example: `$http` becomes `\$http` for regex * * @param str - String to escape * @returns Escaped string safe for regex * @private */ private escapeRegex(str: string): string { return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); } /** * Extracts ALL constructor dependencies (including unused ones). * * Utility method for debugging or analysis. * For Service Bag transform, prefer `extractUsedDependencies()`. * * @param classDecl - Service class declaration * @returns Array of all constructor dependencies * * @example * ```typescript * const all = helper.extractAllDependencies(serviceClass); * console.info(`Service has ${all.length} dependencies`); * ``` */ extractAllDependencies(classDecl: ClassDeclaration): ConstructorDependency[] { const constructor = classDecl.getConstructors()[0]; if (!constructor) { return []; } return this.extractConstructorParameters(constructor); } /** * Checks if a specific dependency is used in methods. * * Utility method for debugging or targeted analysis. * * @param dependencyName - Name of the dependency to check (e.g., 'http', 'logger') * @param methods - Methods to search * @returns True if dependency is used in any method * * @example * ```typescript * const isUsed = helper.isDependencyUsed('http', dataRetrievalMethods); * console.info(`HttpClient used: ${isUsed}`); // true * ``` */ isDependencyUsed( dependencyName: string, methods: MethodDeclaration[], ): boolean { const pattern = new RegExp( String.raw`this\.${this.escapeRegex(dependencyName)}\b`, 'g', ); return methods.some((method) => { const body = method.getBodyText() ?? ''; return pattern.test(body); }); } /** * Groups dependencies by usage across different method groups. * * Useful for understanding which dependencies are shared vs. unique to each group. * * @param classDecl - Service class declaration * @param methodGroups - Map of group name to methods * @returns Map of group name to dependencies used by that group * * @example * ```typescript * const groups = new Map([ * ['data-retrieval', [getUserMethod, fetchOrdersMethod]], * ['validation', [validateEmailMethod]] * ]); * * const depsByGroup = helper.groupDependenciesByUsage(serviceClass, groups); * // Result: { * // 'data-retrieval': [{ name: 'http', type: 'HttpClient', ... }], * // 'validation': [{ name: 'logger', type: 'LoggerService', ... }] * // } * ``` */ groupDependenciesByUsage( classDecl: ClassDeclaration, methodGroups: Map, ): Map { const result = new Map(); for (const [groupName, methods] of methodGroups.entries()) { const dependencies = this.extractUsedDependencies(classDecl, methods); result.set(groupName, dependencies); } return result; } }