import { type ClassDeclaration, type MethodDeclaration, Node } from 'ts-morph'; /** * Represents a cross-service method call detected in a method body. */ export interface CrossServiceCall { /** Method name being called (e.g., 'sortUsers') */ methodName: string; /** Target service that contains the method (e.g., 'TransformationService') */ targetService: string; /** Source method that makes the call */ sourceMethod: MethodDeclaration; /** Full call expression text (e.g., 'this.sortUsers(users)') */ callExpression: string; } /** * Detects cross-service method calls in focused service methods. * * When methods are split across focused services, a method in one service * may call another method that was moved to a different service. * * This detector: * 1. Scans method bodies for `this.methodName()` calls * 2. Checks if methodName exists in the current service * 3. If not, finds which focused service contains it * 4. Returns cross-service call information for injection/rewriting */ export class CrossServiceCallDetector { /** * Detects all cross-service calls in a focused service. * * @param focusedServiceClass - The focused service class to analyze * @param allFocusedServices - Map of all focused services (serviceName → methods) * @returns Array of detected cross-service calls */ detectCrossServiceCalls( focusedServiceClass: ClassDeclaration, allFocusedServices: Map, ): CrossServiceCall[] { const crossServiceCalls: CrossServiceCall[] = []; const currentServiceMethods = new Set( focusedServiceClass.getMethods().map((m) => m.getName()), ); // Analyze each method in the focused service for (const method of focusedServiceClass.getMethods()) { const methodBody = method.getBody(); if (!methodBody) { continue; } // Find all `this.methodName()` calls const thisMethodCalls = this.findThisMethodCalls(methodBody); for (const call of thisMethodCalls) { // Check if method exists in current service if (currentServiceMethods.has(call.methodName)) { continue; // Not a cross-service call } // Find which focused service contains this method const targetService = this.findTargetService( call.methodName, allFocusedServices, ); if (targetService) { crossServiceCalls.push({ methodName: call.methodName, targetService, sourceMethod: method, callExpression: call.callExpression, }); } } } return crossServiceCalls; } /** * Finds all `this.methodName()` calls in a method body. * * Uses AST traversal to find PropertyAccessExpression nodes where: * - Expression is `this` * - Name is a method name * - Parent is a CallExpression * * @param methodBody - Method body node to analyze * @returns Array of method calls with name and full expression */ private findThisMethodCalls( methodBody: Node, ): { methodName: string; callExpression: string }[] { const calls: { methodName: string; callExpression: string }[] = []; methodBody.forEachDescendant((node) => { // Look for PropertyAccessExpression: this.methodName if (Node.isPropertyAccessExpression(node)) { const expression = node.getExpression(); // Check if expression is `this` if (Node.isThisExpression(expression)) { const methodName = node.getName(); // Check if parent is CallExpression (this.methodName()) const parent = node.getParent(); if (Node.isCallExpression(parent)) { calls.push({ methodName, callExpression: parent.getText(), }); } } } }); return calls; } /** * Finds which focused service contains a given method. * * @param methodName - Method name to find * @param allFocusedServices - Map of all focused services * @returns Service name that contains the method, or undefined */ private findTargetService( methodName: string, allFocusedServices: Map, ): string | undefined { for (const [serviceName, methods] of allFocusedServices.entries()) { if (methods.some((m) => m.getName() === methodName)) { return serviceName; } } return undefined; } }