test
This commit is contained in:
@@ -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<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()) {
|
||||
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<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())
|
||||
.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