test
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user