import { type ClassDeclaration, type MethodDeclaration, Node, Scope, } from 'ts-morph'; /** * Represents a private helper method call detected in a method body. */ export interface PrivateHelperCall { /** Helper method name (e.g., 'decodeJwtPayload') */ helperMethodName: string; /** Full call expression (e.g., 'this.decodeJwtPayload(token)') */ callExpression: string; /** The helper method declaration */ helperMethod: MethodDeclaration; } /** * Analysis of how a private helper method is used. */ export interface HelperUsagePattern { /** Type of usage pattern */ usageType: | 'single-responsibility' | 'shared-across-responsibilities' | 'unused'; /** Public methods that call this helper */ usedByMethods: string[]; /** Responsibilities of the calling methods */ usedByResponsibilities: string[]; /** Recommendation for handling this helper */ recommendation: | 'move-with-caller' | 'extract-to-utility' | 'keep-in-original'; } /** * Detects private helper methods called by public methods. * * When public methods are split across focused services, private helper methods * they depend on must also be moved to avoid compilation errors. * * This detector: * 1. Scans public method bodies for `this.privateMethod()` calls * 2. Identifies which private methods are helpers (not public API) * 3. Builds dependency graph for transitive helper calls * 4. Analyzes usage patterns to determine extraction strategy */ export class PrivateHelperMethodDetector { /** * Detects all private helper method calls in a public method. * * @param publicMethod - The public method to analyze * @param classDecl - The class containing the method * @returns Array of detected private helper calls */ detectPrivateHelperCalls( publicMethod: MethodDeclaration, classDecl: ClassDeclaration, ): PrivateHelperCall[] { const helperCalls: PrivateHelperCall[] = []; const methodBody = publicMethod.getBody(); if (!methodBody) { return helperCalls; } // Find all `this.methodName()` calls const thisMethodCalls = this.findThisMethodCalls(methodBody); for (const call of thisMethodCalls) { // Check if method is private const method = classDecl.getMethod(call.methodName); if (method && this.isPrivateHelper(method)) { helperCalls.push({ helperMethodName: call.methodName, callExpression: call.callExpression, helperMethod: method, }); } } return helperCalls; } /** * Detects all transitive private helper dependencies. * * If a public method calls helper1(), and helper1() calls helper2(), * both helpers must be moved together. * * @param publicMethod - The public method to analyze * @param classDecl - The class containing the method * @returns Array of all helper methods (direct + transitive) */ detectTransitiveHelpers( publicMethod: MethodDeclaration, classDecl: ClassDeclaration, ): MethodDeclaration[] { const visited = new Set(); const helpers: MethodDeclaration[] = []; const traverse = (method: MethodDeclaration): void => { const methodName = method.getName(); if (visited.has(methodName)) { return; } visited.add(methodName); const helperCalls = this.detectPrivateHelperCalls(method, classDecl); for (const call of helperCalls) { helpers.push(call.helperMethod); traverse(call.helperMethod); // Recursive traversal } }; traverse(publicMethod); return helpers; } /** * Analyzes how a private helper method is used across the class. * * Determines: * - Is it used by one method or multiple? * - Are the callers from the same responsibility or different? * - Should it be moved with caller or extracted to utility? * * @param helperMethod - The private helper method to analyze * @param classDecl - The class containing the method * @returns Usage pattern analysis */ analyzeHelperUsagePattern( helperMethod: MethodDeclaration, classDecl: ClassDeclaration, ): HelperUsagePattern { const helperName = helperMethod.getName(); const usedByMethods: string[] = []; // Find all methods that call this helper for (const method of classDecl.getMethods()) { if (method === helperMethod) { continue; } // Skip self const body = method.getBody(); if (!body) { continue; } const calls = this.findThisMethodCalls(body); if (calls.some((c) => c.methodName === helperName)) { usedByMethods.push(method.getName()); } } // Determine usage type let usageType: HelperUsagePattern['usageType']; let recommendation: HelperUsagePattern['recommendation']; if (usedByMethods.length === 0) { usageType = 'unused'; recommendation = 'keep-in-original'; // Dead code, but keep for safety } else if (usedByMethods.length === 1) { usageType = 'single-responsibility'; recommendation = 'move-with-caller'; } else { // Multiple callers - need to check if they're from same responsibility // For now, assume shared across responsibilities usageType = 'shared-across-responsibilities'; recommendation = 'extract-to-utility'; } return { usageType, usedByMethods, usedByResponsibilities: [], // TODO: Map methods to responsibilities recommendation, }; } /** * Builds a dependency graph of method calls. * * Maps each method to the methods it calls. * * @param classDecl - The class to analyze * @returns Map of method name to called method names */ buildHelperDependencyGraph( classDecl: ClassDeclaration, ): Map { const graph = new Map(); for (const method of classDecl.getMethods()) { const methodName = method.getName(); const body = method.getBody(); if (!body) { graph.set(methodName, []); continue; } const calls = this.findThisMethodCalls(body); graph.set( methodName, calls.map((c) => c.methodName), ); } // Detect circular dependencies this.detectCircularDependencies(graph); return graph; } /** * Finds all `this.methodName()` calls in a method body. * * @param methodBody - Method body node to analyze * @returns Array of method calls * @private */ private findThisMethodCalls( methodBody: Node, ): { methodName: string; callExpression: string }[] { const calls: { methodName: string; callExpression: string }[] = []; methodBody.forEachDescendant((node) => { if (Node.isPropertyAccessExpression(node)) { const expression = node.getExpression(); if (Node.isThisExpression(expression)) { const methodName = node.getName(); const parent = node.getParent(); if (Node.isCallExpression(parent)) { calls.push({ methodName, callExpression: parent.getText(), }); } } } }); return calls; } /** * Checks if a method is a private helper. * * A method is a private helper if: * - It has private scope * - It's not a lifecycle hook * - It's not a static method * * @param method - Method to check * @returns True if method is a private helper * @private */ private isPrivateHelper(method: MethodDeclaration): boolean { // Must be private if (method.getScope() !== Scope.Private) { return false; } // Exclude lifecycle hooks (even if private) const lifecycleHooks = [ 'ngOnInit', 'ngOnDestroy', 'ngOnChanges', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked', 'ngAfterViewInit', 'ngAfterViewChecked', ]; if (lifecycleHooks.includes(method.getName())) { return false; } // Exclude static methods if (method.isStatic()) { return false; } return true; } /** * Detects circular dependencies in the call graph. * * @param graph - Dependency graph * @throws Error if circular dependency detected * @private */ private detectCircularDependencies(graph: Map): void { const visited = new Set(); const recursionStack = new Set(); const hasCycle = (method: string): boolean => { visited.add(method); recursionStack.add(method); const dependencies = graph.get(method) ?? []; for (const dep of dependencies) { if (!visited.has(dep)) { if (hasCycle(dep)) { return true; } } else if (recursionStack.has(dep)) { return true; // Cycle detected } } recursionStack.delete(method); return false; }; for (const method of Array.from(graph.keys())) { if (!visited.has(method)) { if (hasCycle(method)) { throw new Error(`Circular dependency detected in method call graph`); } } } } }