This commit is contained in:
2026-06-24 06:57:46 +02:00
parent f30538e8c9
commit cb97392bdd
2 changed files with 232 additions and 2 deletions

View File

@@ -992,7 +992,28 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
org.eclipse.jdt.core.dom.ClassInstanceCreation cic, org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context) { 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<String, String> fieldValues = new java.util.HashMap<>(); java.util.Map<String, String> fieldValues = new java.util.HashMap<>();
java.util.Set<String> 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<String, String> fieldValues,
java.util.Set<String> visitedCtors) {
String tdKey = context.getFqn(td);
if (!visitedCtors.add(tdKey)) return;
for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) { for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue; if (!ctor.isConstructor() || ctor.getBody() == null) continue;
@@ -1029,7 +1050,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
fieldName = fa.getName().getIdentifier(); fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& !paramValues.containsKey(sn.getIdentifier())) { && !paramValues.containsKey(sn.getIdentifier())) {
// Bare field assignment (not a parameter-to-itself assignment)
fieldName = sn.getIdentifier(); fieldName = sn.getIdentifier();
} }
@@ -1043,9 +1063,111 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return super.visit(node); 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 break; // first matching constructor is enough
} }
return fieldValues; }
private void processSuperConstructorArgs(
TypeDeclaration superTd,
List<?> superArgs,
java.util.Map<String, String> subClassParamValues,
CodebaseContext context,
java.util.Map<String, String> fieldValues,
java.util.Set<String> 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<String, String> 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<String> 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
}
} }
/** /**

View File

@@ -2991,4 +2991,112 @@ class HeuristicCallGraphEngineTest {
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED"); .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<CallChain> 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");
}
} }