diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index 532625b..a121b1b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -992,7 +992,28 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { org.eclipse.jdt.core.dom.ClassInstanceCreation cic, CodebaseContext context) { + // Use the CIC's actual type as the starting point, not the declared type + String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName); + if (actualTd == null) { + actualTd = td; // Fallback to passed-in type + } + java.util.Map fieldValues = new java.util.HashMap<>(); + java.util.Set visitedCtors = new java.util.HashSet<>(); + buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors); + return fieldValues; + } + + private void buildFieldValuesFromCICRecursive( + org.eclipse.jdt.core.dom.TypeDeclaration td, + org.eclipse.jdt.core.dom.ClassInstanceCreation cic, + CodebaseContext context, + java.util.Map fieldValues, + java.util.Set visitedCtors) { + + String tdKey = context.getFqn(td); + if (!visitedCtors.add(tdKey)) return; for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) { if (!ctor.isConstructor() || ctor.getBody() == null) continue; @@ -1029,7 +1050,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { fieldName = fa.getName().getIdentifier(); } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn && !paramValues.containsKey(sn.getIdentifier())) { - // Bare field assignment (not a parameter-to-itself assignment) fieldName = sn.getIdentifier(); } @@ -1043,9 +1063,111 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return super.visit(node); } }); + + // 3. Follow super constructor chain + ctor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + if (superTd != null) { + processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors); + } + } + return super.visit(sci); + } + }); break; // first matching constructor is enough } - return fieldValues; + } + + private void processSuperConstructorArgs( + TypeDeclaration superTd, + List superArgs, + java.util.Map subClassParamValues, + CodebaseContext context, + java.util.Map fieldValues, + java.util.Set visitedCtors) { + + if (superTd == null) return; + + for (MethodDeclaration superCtor : superTd.getMethods()) { + if (!superCtor.isConstructor() || superCtor.getBody() == null) continue; + if (superCtor.parameters().size() != superArgs.size()) continue; + + // Map super() argument values to superclass constructor parameters + // superArgs may be parameter references from the subclass constructor + java.util.Map superParamValues = new java.util.HashMap<>(); + for (int i = 0; i < superCtor.parameters().size(); i++) { + String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration) + superCtor.parameters().get(i)).getName().getIdentifier(); + org.eclipse.jdt.core.dom.Expression argExpr = + (org.eclipse.jdt.core.dom.Expression) superArgs.get(i); + + // If the argument is a SimpleName (parameter reference), resolve it using subclass paramValues + String resolved = null; + if (argExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + resolved = subClassParamValues.get(sn.getIdentifier()); + } + if (resolved == null) { + java.util.List consts = new java.util.ArrayList<>(); + extractConstantsFromArgument(argExpr, consts); + if (!consts.isEmpty()) { + resolved = consts.get(0); + } else { + resolved = constantResolver.resolve(argExpr, context); + } + } + if (resolved != null) { + superParamValues.put(pName, resolved); + } + } + if (superParamValues.isEmpty()) continue; + + // Scan superclass constructor body for field assignments + superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.Assignment node) { + org.eclipse.jdt.core.dom.Expression lhs = node.getLeftHandSide(); + org.eclipse.jdt.core.dom.Expression rhs = node.getRightHandSide(); + + String fieldName = null; + if (lhs instanceof org.eclipse.jdt.core.dom.FieldAccess fa + && fa.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression) { + fieldName = fa.getName().getIdentifier(); + } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn + && !superParamValues.containsKey(sn.getIdentifier())) { + fieldName = sn.getIdentifier(); + } + + if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) { + String paramVal = superParamValues.get(snRhs.getIdentifier()); + if (paramVal != null) { + fieldValues.put(fieldName, paramVal); + fieldValues.put("this." + fieldName, paramVal); + } + } + return super.visit(node); + } + }); + + // Recursively follow further super() calls, passing down the current superclass paramValues + superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) { + String superFqn = context.getSuperclassFqn(superTd); + if (superFqn != null) { + TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn); + if (nextSuperTd != null) { + processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors); + } + } + return super.visit(sci); + } + }); + break; // first matching super constructor is enough + } } /** diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index 5299437..781e47f 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -2991,4 +2991,112 @@ class HeuristicCallGraphEngineTest { assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) .containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED"); } + /** + * Diagnostic: Complex multi-argument initialization with abstract base class mapping. + * + * Pattern: + * 1. A Factory creates a specific subclass (WebCommand), passing multiple arguments + * including an intermediate enum (SourceType.WEB_API) alongside strings and ints. + * 2. The subclass delegates to an abstract BaseCommand via super(...). + * 3. The base class stores the intermediate enum in a protected field. + * 4. The controller calls an inherited method: cmd.getTransitionEvent() + * 5. The inherited method uses a switch on the protected field to return a TransitionEnum. + * + * Root cause targeted: The engine traces the field back to the constructor and finds + * SourceType.WEB_API. If the engine is buggy, it will eagerly return "SourceType.WEB_API". + * If correct, it will evaluate the switch mapping and return "TransitionEnum.ON_WEB_START". + */ + @Test + void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException { + String source = """ + package com.example; + + public class FlowController { + private CommandFactory factory; + private StateMachine machine; + + public void handleRequest(String payload) { + // Creates subclass with multiple args, including intermediate enum + BaseCommand cmd = factory.buildCommand(payload); + + // Fires using the mapped enum from the inherited base method + machine.fire(cmd.getTransitionEvent()); + } + } + + class CommandFactory { + public BaseCommand buildCommand(String payload) { + // Passing multiple args: String, Enum1, String, int + return new WebCommand(payload, SourceType.WEB_API, "meta_v1", 42); + } + } + + abstract class BaseCommand { + protected String id; + protected SourceType sourceType; // The intermediate enum + protected String metadata; + + public BaseCommand(String id, SourceType sourceType, String metadata) { + this.id = id; + this.sourceType = sourceType; + this.metadata = metadata; + } + + // The tricky part: Inherited method doing the mapping + public TransitionEnum getTransitionEvent() { + return switch (this.sourceType) { + case WEB_API -> TransitionEnum.ON_WEB_START; + case MOBILE_APP -> TransitionEnum.ON_MOBILE_START; + case CRON_JOB -> TransitionEnum.ON_SYSTEM_START; + }; + } + } + + class WebCommand extends BaseCommand { + private int priorityLevel; + + public WebCommand(String id, SourceType sourceType, String metadata, int priorityLevel) { + super(id, sourceType, metadata); // Engine must track through super() + this.priorityLevel = priorityLevel; + } + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum SourceType { WEB_API, MOBILE_APP, CRON_JOB } + enum TransitionEnum { ON_WEB_START, ON_MOBILE_START, ON_SYSTEM_START } + """; + java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_complex_inheritance"); + java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.FlowController") + .methodName("handleRequest") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.StateMachine") + .methodName("fire") + .event("event") + .build(); + + List chains = engine.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + + System.out.println("COMPLEX INHERITANCE polymorphicEvents: " + + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // The assertion to prove the bug is fixed. + // It MUST NOT contain "SourceType.WEB_API" + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("TransitionEnum.ON_WEB_START"); + } }