forward analysis nemotron3 ultra
This commit is contained in:
@@ -27,7 +27,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||
hasPolyMatch = true;
|
||||
break;
|
||||
}
|
||||
continue; // Stricter matching: do not fallback if both have qualifiers but don't match
|
||||
continue;
|
||||
}
|
||||
|
||||
String simplePe = pe;
|
||||
|
||||
@@ -56,6 +56,22 @@ public class SymbolicPathValueEstimator implements PathValueEstimator {
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(InfixExpression node) {
|
||||
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String left = resolver.resolve(node.getLeftOperand(), context);
|
||||
if (left != null && !left.isEmpty()) {
|
||||
addValue(collectedConstants, left);
|
||||
}
|
||||
String right = resolver.resolve(node.getRightOperand(), context);
|
||||
if (right != null && !right.isEmpty()) {
|
||||
addValue(collectedConstants, right);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SwitchStatement node) {
|
||||
for (Object stmt : node.statements()) {
|
||||
|
||||
@@ -108,7 +108,32 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
TypeDeclaration td = null;
|
||||
if (mi.getExpression() != null) {
|
||||
ITypeBinding typeBinding = mi.getExpression().resolveTypeBinding();
|
||||
if (typeBinding != null) {
|
||||
String typeName = typeBinding.getQualifiedName();
|
||||
if (typeName != null && !typeName.isEmpty()) {
|
||||
td = context.getTypeDeclaration(typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (td == null && mi.getExpression() instanceof SimpleName sn) {
|
||||
String typeName = resolveLocalType(sn);
|
||||
if (typeName != null) {
|
||||
td = context.getTypeDeclaration(typeName);
|
||||
if (td == null) {
|
||||
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||
td = context.getTypeDeclaration(typeName, cu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (td == null) {
|
||||
td = findEnclosingType(mi);
|
||||
}
|
||||
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||
if (md != null) {
|
||||
@@ -132,55 +157,145 @@ public class ConstantResolver {
|
||||
private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set<String> visited) {
|
||||
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
|
||||
|
||||
java.util.Map<String, String> localVars = new java.util.HashMap<>();
|
||||
for (int i = 0; i < mi.arguments().size(); i++) {
|
||||
Expression arg = (Expression) mi.arguments().get(i);
|
||||
String val = resolveInternal(arg, context, visited);
|
||||
if (val != null) {
|
||||
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
|
||||
localVars.put(param.getName().getIdentifier(), val);
|
||||
}
|
||||
TypeDeclaration td = findEnclosingType(md);
|
||||
if (td == null) return null;
|
||||
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
|
||||
if (!visited.add(methodFqn)) {
|
||||
return null; // Cycle detected in method evaluation
|
||||
}
|
||||
|
||||
if (localVars.isEmpty()) return null;
|
||||
try {
|
||||
java.util.Map<String, String> localVars = new java.util.HashMap<>();
|
||||
java.util.Set<String> declaredLocals = new java.util.HashSet<>();
|
||||
|
||||
for (Object stmtObj : md.getBody().statements()) {
|
||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||
if (es.getExpression() instanceof Assignment assignment) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
String varName = null;
|
||||
if (lhs instanceof SimpleName sn) {
|
||||
varName = sn.getIdentifier();
|
||||
} else if (lhs instanceof FieldAccess fa) {
|
||||
varName = "this." + fa.getName().getIdentifier();
|
||||
}
|
||||
|
||||
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||
|
||||
if (varName != null && rhsVal != null) {
|
||||
localVars.put(varName, rhsVal);
|
||||
if (varName.startsWith("this.")) {
|
||||
localVars.put(varName.substring(5), rhsVal); // alias without 'this.'
|
||||
} else {
|
||||
localVars.put("this." + varName, rhsVal); // alias with 'this.'
|
||||
for (int i = 0; i < mi.arguments().size(); i++) {
|
||||
Expression arg = (Expression) mi.arguments().get(i);
|
||||
String val = resolveInternal(arg, context, visited);
|
||||
if (val != null) {
|
||||
org.eclipse.jdt.core.dom.SingleVariableDeclaration param =
|
||||
(org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
|
||||
String paramName = param.getName().getIdentifier();
|
||||
localVars.put(paramName, val);
|
||||
declaredLocals.add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
|
||||
} finally {
|
||||
visited.remove(methodFqn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a method body with pre-supplied local values (e.g. field values derived
|
||||
* from the specific {@code ClassInstanceCreation} that created the receiver object).
|
||||
* Used by {@link click.kamil.springstatemachineexporter.analysis.service.AbstractCallGraphEngine}
|
||||
* to resolve transforming getters — methods that map a constructor-injected enum to a
|
||||
* different state-machine enum via switch expressions.
|
||||
*/
|
||||
public String evaluateMethodBodyWithLocals(MethodDeclaration md,
|
||||
java.util.Map<String, String> prebuiltLocals,
|
||||
CodebaseContext context,
|
||||
Set<String> visited) {
|
||||
if (md.getBody() == null) return null;
|
||||
|
||||
TypeDeclaration td = findEnclosingType(md);
|
||||
if (td == null) return null;
|
||||
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
|
||||
if (!visited.add(methodFqn)) {
|
||||
return null; // Cycle detected in method evaluation
|
||||
}
|
||||
|
||||
try {
|
||||
java.util.Map<String, String> localVars = new java.util.HashMap<>(prebuiltLocals);
|
||||
java.util.Set<String> declaredLocals = new java.util.HashSet<>(prebuiltLocals.keySet());
|
||||
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
|
||||
} finally {
|
||||
visited.remove(methodFqn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core body-evaluation logic shared by {@link #evaluateMethodOutput} and
|
||||
* {@link #evaluateMethodBodyWithLocals}. Visits the block, tracks assignments,
|
||||
* evaluates switch statements/expressions, and captures the first resolved return value.
|
||||
*/
|
||||
private String evaluateBody(org.eclipse.jdt.core.dom.Block body,
|
||||
java.util.Map<String, String> localVars,
|
||||
java.util.Set<String> declaredLocals,
|
||||
CodebaseContext context,
|
||||
Set<String> visited) {
|
||||
String[] finalResult = new String[1];
|
||||
|
||||
body.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement vds) {
|
||||
for (Object fragmentObj : vds.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||
String varName = fragment.getName().getIdentifier();
|
||||
declaredLocals.add(varName);
|
||||
if (fragment.getInitializer() != null) {
|
||||
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
|
||||
if (rhsVal != null) localVars.put(varName, rhsVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
||||
String result = evaluateSwitchStatement(ss, localVars, context, visited);
|
||||
if (result != null) return result;
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
||||
String result = evaluateSwitchExpression(se, localVars, context, visited);
|
||||
if (result != null) return result;
|
||||
} else {
|
||||
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return super.visit(vds);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@Override
|
||||
public boolean visit(Assignment assignment) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
String varName = null;
|
||||
if (lhs instanceof SimpleName sn) {
|
||||
varName = sn.getIdentifier();
|
||||
} else if (lhs instanceof FieldAccess fa) {
|
||||
varName = "this." + fa.getName().getIdentifier();
|
||||
}
|
||||
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||
if (varName != null && rhsVal != null) {
|
||||
if (varName.startsWith("this.")) {
|
||||
localVars.put(varName, rhsVal);
|
||||
String bareName = varName.substring(5);
|
||||
if (!declaredLocals.contains(bareName)) localVars.put(bareName, rhsVal);
|
||||
} else {
|
||||
localVars.put(varName, rhsVal);
|
||||
if (!declaredLocals.contains(varName)) localVars.put("this." + varName, rhsVal);
|
||||
}
|
||||
}
|
||||
return super.visit(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
||||
if (finalResult[0] == null) {
|
||||
String result = evaluateSwitchStatement(ss, localVars, context, visited);
|
||||
if (result != null) finalResult[0] = result;
|
||||
}
|
||||
return false; // children handled inside evaluateSwitchStatement
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
if (finalResult[0] == null) {
|
||||
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
||||
String result = evaluateSwitchExpression(se, localVars, context, visited);
|
||||
if (result != null) finalResult[0] = result;
|
||||
} else if (rs.getExpression() != null) {
|
||||
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
|
||||
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
|
||||
if (result != null) finalResult[0] = result;
|
||||
}
|
||||
}
|
||||
return super.visit(rs);
|
||||
}
|
||||
});
|
||||
|
||||
return finalResult[0];
|
||||
}
|
||||
|
||||
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
||||
@@ -196,6 +311,10 @@ public class ConstantResolver {
|
||||
} else {
|
||||
for (Object exprObj : sc.expressions()) {
|
||||
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
|
||||
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
|
||||
caseVal = caseSn.getIdentifier();
|
||||
}
|
||||
if (caseVal != null && switchVar.endsWith(caseVal)) {
|
||||
matchingCase = true;
|
||||
break;
|
||||
@@ -228,6 +347,10 @@ public class ConstantResolver {
|
||||
} else {
|
||||
for (Object exprObj : sc.expressions()) {
|
||||
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
|
||||
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
|
||||
caseVal = caseSn.getIdentifier();
|
||||
}
|
||||
if (caseVal != null && switchVar.endsWith(caseVal)) {
|
||||
matchingCase = true;
|
||||
break;
|
||||
@@ -251,7 +374,7 @@ public class ConstantResolver {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String name = sn.getIdentifier();
|
||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||
return name;
|
||||
return null;
|
||||
} else if (expr instanceof FieldAccess fa) {
|
||||
String name = "this." + fa.getName().getIdentifier();
|
||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||
@@ -410,4 +533,49 @@ public class ConstantResolver {
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof MethodDeclaration md) {
|
||||
return md;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveLocalType(SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
|
||||
if (enclosingMethod != null && enclosingMethod.getBody() != null) {
|
||||
String[] typeName = new String[1];
|
||||
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
typeName[0] = node.getType().toString();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (typeName[0] != null) return typeName[0];
|
||||
}
|
||||
|
||||
TypeDeclaration enclosingType = findEnclosingType(sn);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return field.getType().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
} else {
|
||||
// Fallback for complex chained expressions
|
||||
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
|
||||
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this";
|
||||
if (!exprStr.contains("(")) {
|
||||
varName = exprStr;
|
||||
}
|
||||
@@ -256,7 +256,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
||||
} else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
|
||||
varName = sn.getIdentifier();
|
||||
methodName = "VariableReference"; // We just want to trigger deep trace
|
||||
// Check if it's a constant (Java UPPER_SNAKE_CASE convention)
|
||||
// If so, add it directly to polymorphicEvents instead of tracing as a variable
|
||||
if (varName.equals(varName.toUpperCase()) && varName.contains("_")) {
|
||||
polymorphicEvents.add(varName);
|
||||
methodName = null; // Skip variable tracing
|
||||
} else {
|
||||
methodName = "VariableReference"; // We just want to trigger deep trace
|
||||
}
|
||||
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
|
||||
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
|
||||
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
@@ -277,10 +284,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
|
||||
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
|
||||
extractConstantsFromExpression(tracedSetter, polymorphicEvents);
|
||||
if (tracedSetter instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
|
||||
resolvedValue = cic.toString() + "." + methodName + "()";
|
||||
}
|
||||
}
|
||||
if (!polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(tp.getEvent())
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
@@ -295,30 +305,91 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
if (varName != null && declaredType == null) {
|
||||
for (String methodFqn : path) {
|
||||
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
if (declaredType != null) {
|
||||
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
List<String> values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
|
||||
System.out.println("DEBUG resolveMethodReturnConstant returned: " + values);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
polymorphicEvents.addAll(values);
|
||||
if (!varName.isEmpty() && !"this".equals(varName) && !"super".equals(varName)) {
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(tp.getClassName());
|
||||
if (currentTd != null) {
|
||||
MethodDeclaration currentMd = context.findMethodDeclaration(currentTd, tp.getMethodName(), true);
|
||||
if (currentMd != null) {
|
||||
final String[] extractedFinalTraced = {null, null};
|
||||
final String targetVar = varName;
|
||||
final String mName = methodName;
|
||||
final String[] finalDeclaredType = {null};
|
||||
final String[] finalSourceMethod = {null};
|
||||
currentMd.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) {
|
||||
if (vdf.getInitializer() != null) {
|
||||
org.eclipse.jdt.core.dom.Expression valueNode = traceVariable(vdf.getInitializer());
|
||||
if (valueNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
extractedFinalTraced[0] = cic.toString();
|
||||
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
|
||||
} else {
|
||||
finalDeclaredType[0] = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
finalSourceMethod[0] = "inline-instantiation";
|
||||
}
|
||||
} else {
|
||||
extractedFinalTraced[0] = valueNode.toString();
|
||||
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (finalDeclaredType[0] != null) {
|
||||
declaredType = finalDeclaredType[0];
|
||||
sourceMethod = finalSourceMethod[0];
|
||||
}
|
||||
if (extractedFinalTraced[0] != null) {
|
||||
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
|
||||
System.out.println("DEBUG localized tracing resolvedValue to: " + resolvedValue);
|
||||
// Since we updated resolvedValue, we need to extract from it if it's an inline anonymous class
|
||||
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
|
||||
org.eclipse.jdt.core.dom.ASTParser p = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
|
||||
p.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
|
||||
p.setSource(resolvedValue.toCharArray());
|
||||
org.eclipse.jdt.core.dom.ASTNode newNode = p.createAST(null);
|
||||
if (newNode instanceof org.eclipse.jdt.core.dom.Expression) {
|
||||
List<org.eclipse.jdt.core.dom.Expression> traced = traceVariableAll((org.eclipse.jdt.core.dom.Expression) newNode);
|
||||
for (org.eclipse.jdt.core.dom.Expression ex : traced) {
|
||||
extractConstantsFromExpression(ex, polymorphicEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceMethod = methodFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
|
||||
if (declaredType == null && varName.matches("^[A-Z].*")) {
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
|
||||
if (staticTd != null) {
|
||||
declaredType = context.getFqn(staticTd);
|
||||
sourceMethod = "static-call";
|
||||
} else if (context.getEnumValues(varName) != null) {
|
||||
declaredType = varName;
|
||||
sourceMethod = "static-call";
|
||||
|
||||
if (declaredType == null) {
|
||||
for (String methodFqn : path) {
|
||||
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
if (declaredType != null) {
|
||||
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
List<String> values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
|
||||
System.out.println("DEBUG resolveMethodReturnConstant returned: " + values);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
polymorphicEvents.addAll(values);
|
||||
}
|
||||
sourceMethod = methodFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
|
||||
if (declaredType == null && varName.matches("^[A-Z].*")) {
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
|
||||
if (staticTd != null) {
|
||||
declaredType = context.getFqn(staticTd);
|
||||
sourceMethod = "static-call";
|
||||
} else if (context.getEnumValues(varName) != null) {
|
||||
declaredType = varName;
|
||||
sourceMethod = "static-call";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -868,7 +939,19 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
|
||||
// Try proper getter evaluation: builds field→value from constructor args,
|
||||
// then evaluates the getter body (handles switch-based enum transformations).
|
||||
java.util.List<String> narrowed = resolveInlineInstantiationGetterResult(
|
||||
cic, methodName, context, new java.util.HashSet<>());
|
||||
if (!narrowed.isEmpty()) {
|
||||
constants.addAll(narrowed);
|
||||
handledByLombok = true; // reuse flag to skip fallback below
|
||||
}
|
||||
}
|
||||
if (!handledByLombok) {
|
||||
// Fallback: extract all constructor args (catches cases where getter
|
||||
// evaluation is not possible, e.g. no source for the wrapper class)
|
||||
for (Object argObj : cic.arguments()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
|
||||
}
|
||||
@@ -897,6 +980,164 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a map of {@code fieldName → resolved-value} (and {@code this.fieldName → value})
|
||||
* by correlating the actual arguments of the given {@link ClassInstanceCreation} with the
|
||||
* field assignments found in the matching constructor body.
|
||||
*
|
||||
* <p>Handles both {@code this.field = param} and bare {@code field = param} assignment forms.
|
||||
*/
|
||||
protected java.util.Map<String, String> buildFieldValuesFromCIC(
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td,
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
|
||||
CodebaseContext context) {
|
||||
|
||||
java.util.Map<String, String> fieldValues = new java.util.HashMap<>();
|
||||
|
||||
for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) {
|
||||
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
||||
if (ctor.parameters().size() != cic.arguments().size()) continue;
|
||||
|
||||
// 1. Map each parameter name → resolved argument value
|
||||
java.util.Map<String, String> paramValues = new java.util.HashMap<>();
|
||||
for (int i = 0; i < ctor.parameters().size(); i++) {
|
||||
String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration)
|
||||
ctor.parameters().get(i)).getName().getIdentifier();
|
||||
org.eclipse.jdt.core.dom.Expression argExpr =
|
||||
(org.eclipse.jdt.core.dom.Expression) cic.arguments().get(i);
|
||||
java.util.List<String> consts = new java.util.ArrayList<>();
|
||||
extractConstantsFromArgument(argExpr, consts);
|
||||
if (!consts.isEmpty()) {
|
||||
paramValues.put(pName, consts.get(0));
|
||||
} else {
|
||||
String resolved = constantResolver.resolve(argExpr, context);
|
||||
if (resolved != null) paramValues.put(pName, resolved);
|
||||
}
|
||||
}
|
||||
if (paramValues.isEmpty()) continue;
|
||||
|
||||
// 2. Scan constructor body for field assignments: this.field = param OR field = param
|
||||
ctor.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
|
||||
&& !paramValues.containsKey(sn.getIdentifier())) {
|
||||
// Bare field assignment (not a parameter-to-itself assignment)
|
||||
fieldName = sn.getIdentifier();
|
||||
}
|
||||
|
||||
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
|
||||
String paramVal = paramValues.get(snRhs.getIdentifier());
|
||||
if (paramVal != null) {
|
||||
fieldValues.put(fieldName, paramVal);
|
||||
fieldValues.put("this." + fieldName, paramVal);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
break; // first matching constructor is enough
|
||||
}
|
||||
return fieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a {@code return new SomeType(...)} statement in the given method, recursing
|
||||
* into interface implementations when the method body is absent.
|
||||
*/
|
||||
protected org.eclipse.jdt.core.dom.ClassInstanceCreation findReturnedCIC(
|
||||
String className, String methodName, CodebaseContext context) {
|
||||
return findReturnedCIC(className, methodName, context, new java.util.HashSet<>(), 0);
|
||||
}
|
||||
|
||||
private org.eclipse.jdt.core.dom.ClassInstanceCreation findReturnedCIC(
|
||||
String className, String methodName, CodebaseContext context,
|
||||
java.util.Set<String> visited, int depth) {
|
||||
if (depth > 50 || !visited.add(className + "#" + methodName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
org.eclipse.jdt.core.dom.MethodDeclaration md =
|
||||
context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation[] result = {null};
|
||||
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
result[0] = cic;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (result[0] != null) return result[0];
|
||||
}
|
||||
}
|
||||
// Recurse into implementations (handles interfaces / abstract classes)
|
||||
java.util.List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1);
|
||||
if (cic != null) return cic;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the value produced by calling {@code getterName} on an object created via the
|
||||
* given {@link ClassInstanceCreation}.
|
||||
*
|
||||
* <p>Algorithm:
|
||||
* <ol>
|
||||
* <li>Build a {@code fieldName → value} map from the CIC's constructor arguments.</li>
|
||||
* <li>Ask {@link click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver}
|
||||
* to evaluate the getter body with those values as pre-seeded locals.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>This correctly handles both simple pass-through getters ({@code return field}) and
|
||||
* transformation getters that map one enum to another via a switch expression.
|
||||
*
|
||||
* @return resolved values, or an empty list when resolution is not possible
|
||||
*/
|
||||
private java.util.List<String> resolveInlineInstantiationGetterResult(
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
|
||||
String getterName,
|
||||
CodebaseContext context,
|
||||
java.util.Set<String> visited) {
|
||||
|
||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.extractSimpleTypeName(cic.getType());
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(typeName);
|
||||
if (td == null) return java.util.Collections.emptyList();
|
||||
|
||||
org.eclipse.jdt.core.dom.MethodDeclaration getter =
|
||||
context.findMethodDeclaration(td, getterName, true);
|
||||
if (getter == null || getter.getBody() == null) return java.util.Collections.emptyList();
|
||||
|
||||
String getterKey = typeName + "#" + getterName;
|
||||
if (!visited.add(getterKey)) {
|
||||
return java.util.Collections.emptyList(); // Cycle detected
|
||||
}
|
||||
|
||||
try {
|
||||
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context);
|
||||
if (fieldValues.isEmpty()) return java.util.Collections.emptyList();
|
||||
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||
return result != null ? java.util.List.of(result) : java.util.Collections.emptyList();
|
||||
} finally {
|
||||
visited.remove(getterKey);
|
||||
}
|
||||
}
|
||||
|
||||
private int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
|
||||
boolean isAllArgsConstructor = false;
|
||||
boolean isRequiredArgsConstructor = false;
|
||||
@@ -1371,7 +1612,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
String calledName = mi.getName().getIdentifier();
|
||||
String targetClass = context.getFqn(td);
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||
} else {
|
||||
String val = constantResolver.resolve(tracedRight, context);
|
||||
if (val != null) results.add(val);
|
||||
@@ -1407,7 +1648,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||
} else {
|
||||
String val = constantResolver.resolve(tracedRight, context);
|
||||
if (val != null) results.add(val);
|
||||
|
||||
@@ -265,8 +265,26 @@ public class GenericEventDetector {
|
||||
return getSimpleNameString(right);
|
||||
}
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
||||
return getSimpleNameString((Expression) mi.arguments().get(0));
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
|
||||
Expression receiver = mi.getExpression();
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
|
||||
// If receiver is null (e.g., implicit this), fall back to arg
|
||||
if (receiver == null) {
|
||||
return getSimpleNameString(arg);
|
||||
}
|
||||
|
||||
// Prioritize the one that looks like a constant (QualifiedName or StringLiteral)
|
||||
if (receiver instanceof QualifiedName || receiver instanceof StringLiteral || receiver instanceof FieldAccess) {
|
||||
return getSimpleNameString(receiver);
|
||||
}
|
||||
if (arg instanceof QualifiedName || arg instanceof StringLiteral || arg instanceof FieldAccess) {
|
||||
return getSimpleNameString(arg);
|
||||
}
|
||||
|
||||
// Fallback to receiver
|
||||
return getSimpleNameString(receiver);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -162,8 +162,19 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
expr = firstArg; // Only unwrap if it's actually a constant
|
||||
}
|
||||
}
|
||||
|
||||
String val = constantResolver.resolve(expr, context);
|
||||
|
||||
String val = null;
|
||||
|
||||
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
|
||||
// from a ClassInstanceCreation (directly or via a factory method). This avoids
|
||||
// the broad ENUM_SET fallback when the getter transforms one enum to another.
|
||||
if (expr instanceof MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof SimpleName instanceSn
|
||||
&& getterMi.arguments().isEmpty()) {
|
||||
val = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
|
||||
}
|
||||
|
||||
if (val == null) val = constantResolver.resolve(expr, context);
|
||||
if (val == null) {
|
||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||
}
|
||||
@@ -317,4 +328,163 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
|
||||
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
|
||||
* (either directly or via a factory/mapper method).
|
||||
*
|
||||
* <p>This handles both:
|
||||
* <ul>
|
||||
* <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
|
||||
* <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the resolved constant string, or {@code null} when narrowing is not possible
|
||||
*/
|
||||
private String tryNarrowGetterViaLocalVarChain(
|
||||
MethodInvocation getterCall, SimpleName instanceSn) {
|
||||
|
||||
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
|
||||
if (enclosing == null || enclosing.getBody() == null) return null;
|
||||
|
||||
String varName = instanceSn.getIdentifier();
|
||||
String getterName = getterCall.getName().getIdentifier();
|
||||
|
||||
// Find the declared type and initialiser of the local variable
|
||||
String[] varTypeName = {null};
|
||||
Expression[] initExpr = {null};
|
||||
enclosing.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement vds) {
|
||||
for (Object fragObj : vds.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
varTypeName[0] = vds.getType().toString();
|
||||
initExpr[0] = frag.getInitializer();
|
||||
}
|
||||
}
|
||||
return super.visit(vds);
|
||||
}
|
||||
});
|
||||
if (varTypeName[0] == null) return null;
|
||||
|
||||
// Obtain the ClassInstanceCreation that created the object
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
|
||||
if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
|
||||
cic = directCic;
|
||||
} else if (initExpr[0] instanceof MethodInvocation factoryMi
|
||||
&& factoryMi.getExpression() instanceof SimpleName receiverSn) {
|
||||
// e.g. "mapper.toMsg(dto)" → find the CIC returned by the factory method
|
||||
String receiverType = resolveReceiverTypeFallback(receiverSn);
|
||||
if (receiverType != null) {
|
||||
cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
|
||||
}
|
||||
}
|
||||
if (cic == null) return null;
|
||||
|
||||
// Resolve the variable's declared type to a TypeDeclaration
|
||||
CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
|
||||
TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
|
||||
if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
|
||||
if (varTypeTd == null) return null;
|
||||
|
||||
// Evaluate the getter body with field values substituted from the CIC's constructor args
|
||||
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context);
|
||||
if (fieldValues.isEmpty()) return null;
|
||||
|
||||
// Track setter calls on the variable between its initialization and the getter call
|
||||
// to capture field updates like e.setType("NEW_VALUE")
|
||||
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
|
||||
|
||||
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
|
||||
if (getter == null || getter.getBody() == null) return null;
|
||||
|
||||
return constantResolver.evaluateMethodBodyWithLocals(
|
||||
getter, fieldValues, context, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the method body for setter calls or field assignments to the given variable
|
||||
* that occur between the variable's initialization and the getter call.
|
||||
* Updates fieldValues with any field values set via setters.
|
||||
*/
|
||||
private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
|
||||
org.eclipse.jdt.core.dom.MethodInvocation getterCall,
|
||||
java.util.Map<String, String> fieldValues,
|
||||
CodebaseContext context) {
|
||||
java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
|
||||
for (Object stmtObj : methodBody.statements()) {
|
||||
statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
|
||||
}
|
||||
|
||||
int varDeclIndex = -1;
|
||||
int getterCallIndex = -1;
|
||||
for (int i = 0; i < statements.size(); i++) {
|
||||
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||
// Find variable declaration
|
||||
if (varDeclIndex == -1) {
|
||||
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
|
||||
for (Object fragObj : vds.fragments()) {
|
||||
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
|
||||
&& frag.getName().getIdentifier().equals(varName)) {
|
||||
varDeclIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find getter call
|
||||
if (getterCallIndex == -1) {
|
||||
if (stmt.toString().contains(getterCall.toString())) {
|
||||
getterCallIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
|
||||
// Scan statements between var declaration and getter call
|
||||
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
|
||||
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||
org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
|
||||
// Check for setter calls: var.setField(value)
|
||||
if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||
&& sn.getIdentifier().equals(varName)) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
// Check if it's a setter (setXxx or xxx)
|
||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||
String fieldName = Character.toLowerCase(methodName.charAt(3))
|
||||
+ methodName.substring(4);
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
|
||||
String val = constantResolver.resolve(arg, context);
|
||||
if (val != null) {
|
||||
fieldValues.put(fieldName, val);
|
||||
fieldValues.put("this." + fieldName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for direct field assignments: var.field = value
|
||||
else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
|
||||
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
|
||||
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||
&& sn.getIdentifier().equals(varName)) {
|
||||
String fieldName = fa.getName().getIdentifier();
|
||||
org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
|
||||
String val = constantResolver.resolve(rhs, context);
|
||||
if (val != null) {
|
||||
fieldValues.put(fieldName, val);
|
||||
fieldValues.put("this." + fieldName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
|
||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||
Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||
if (unwrappedBuilder != expr) {
|
||||
expr = unwrappedBuilder;
|
||||
}
|
||||
|
||||
Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||
@@ -369,4 +374,22 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression unwrapMessageBuilder(Expression expr) {
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
MethodInvocation current = mi;
|
||||
while (current != null) {
|
||||
String name = current.getName().getIdentifier();
|
||||
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
||||
return (Expression) current.arguments().get(0);
|
||||
}
|
||||
Expression receiver = current.getExpression();
|
||||
if (receiver instanceof MethodInvocation nextMi) {
|
||||
current = nextMi;
|
||||
} else {
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user