Compare commits
12 Commits
working_bu
...
fcda626629
| Author | SHA1 | Date | |
|---|---|---|---|
| fcda626629 | |||
| e31056a44e | |||
| 131ccbeda0 | |||
| 14b311be2b | |||
| 1b690582d6 | |||
| 3ca834fa71 | |||
| c6b4a4be1e | |||
| 24a9be3a4e | |||
| 07d241a060 | |||
| 32de0a51c6 | |||
| cbb00e8a0f | |||
| 6596303b7d |
@@ -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;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class ForwardPathEstimationEnricher implements AnalysisEnricher {
|
||||
|
||||
private final boolean isEnabled;
|
||||
private final PathValueEstimator estimator;
|
||||
|
||||
public ForwardPathEstimationEnricher() {
|
||||
this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false"));
|
||||
this.estimator = new SymbolicPathValueEstimator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Running Symbolic Forward Path Analysis for {}", result.getName());
|
||||
|
||||
List<CallChain> chains = result.getMetadata().getCallChains();
|
||||
List<CallChain> updatedChains = chains.stream().map(chain -> {
|
||||
if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain;
|
||||
|
||||
// If the event looks like a dynamic variable (no dots, not an enum format)
|
||||
if (!chain.getTriggerPoint().getEvent().contains(".")) {
|
||||
List<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
|
||||
if (estimatedValues != null && !estimatedValues.isEmpty()) {
|
||||
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
|
||||
|
||||
List<String> polyEvents = new ArrayList<>();
|
||||
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||
polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
polyEvents.addAll(estimatedValues);
|
||||
|
||||
TriggerPoint newTp = chain.getTriggerPoint().toBuilder()
|
||||
.polymorphicEvents(polyEvents)
|
||||
.build();
|
||||
|
||||
return chain.toBuilder().triggerPoint(newTp).build();
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}).collect(Collectors.toList());
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata =
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.triggers(result.getMetadata().getTriggers())
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
.callChains(updatedChains)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build();
|
||||
|
||||
result.setMetadata(updatedMetadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PathValueEstimator {
|
||||
List<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class SymbolicPathValueEstimator implements PathValueEstimator {
|
||||
|
||||
@Override
|
||||
public List<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
|
||||
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> collectedConstants = new HashSet<>();
|
||||
|
||||
for (String methodFqn : chain.getMethodChain()) {
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot == -1) continue;
|
||||
|
||||
String className = methodFqn.substring(0, lastDot);
|
||||
String methodName = methodFqn.substring(lastDot + 1);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) continue;
|
||||
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null || md.getBody() == null) continue;
|
||||
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase"))
|
||||
&& node.arguments().size() == 1) {
|
||||
|
||||
Expression caller = node.getExpression();
|
||||
Expression arg = (Expression) node.arguments().get(0);
|
||||
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
|
||||
String callerVal = resolver.resolve(caller, context);
|
||||
if (callerVal != null && !callerVal.isEmpty()) {
|
||||
addValue(collectedConstants, callerVal);
|
||||
}
|
||||
|
||||
String argVal = resolver.resolve(arg, context);
|
||||
if (argVal != null && !argVal.isEmpty()) {
|
||||
addValue(collectedConstants, argVal);
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||
for (Object expr : sc.expressions()) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String val = resolver.resolve((Expression) expr, context);
|
||||
if (val != null && !val.isEmpty()) {
|
||||
addValue(collectedConstants, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SwitchExpression node) {
|
||||
for (Object stmt : node.statements()) {
|
||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||
for (Object expr : sc.expressions()) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String val = resolver.resolve((Expression) expr, context);
|
||||
if (val != null && !val.isEmpty()) {
|
||||
addValue(collectedConstants, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (collectedConstants.isEmpty()) return null;
|
||||
return new ArrayList<>(collectedConstants);
|
||||
}
|
||||
|
||||
private void addValue(Set<String> collectedConstants, String val) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
String parsed = parseEnumSetElement(eVal);
|
||||
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
|
||||
}
|
||||
} else {
|
||||
collectedConstants.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
private String parseEnumSetElement(String element) {
|
||||
if (element.contains(".")) {
|
||||
return element.substring(element.lastIndexOf('.') + 1);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@Builder(toBuilder = true)
|
||||
@Jacksonized
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TriggerPoint {
|
||||
|
||||
@@ -64,6 +64,9 @@ public class ConstantResolver {
|
||||
String val = resolveManual(qn, context, visited);
|
||||
return val != null ? val : qn.toString();
|
||||
}
|
||||
if (expr instanceof FieldAccess fa) {
|
||||
return resolveManual(fa.getName(), context, visited);
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return resolveManual(sn, context, visited);
|
||||
}
|
||||
@@ -88,6 +91,13 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
|
||||
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
|
||||
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
|
||||
if (enumValues != null && !enumValues.isEmpty()) {
|
||||
return "ENUM_SET:" + String.join(",", enumValues);
|
||||
}
|
||||
}
|
||||
|
||||
IMethodBinding mb = mi.resolveMethodBinding();
|
||||
if (mb != null) {
|
||||
ITypeBinding returnType = mb.getReturnType();
|
||||
@@ -98,10 +108,39 @@ 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 && md.getReturnType2() != null) {
|
||||
if (md != null) {
|
||||
String evaluated = evaluateMethodOutput(mi, md, context, visited);
|
||||
if (evaluated != null) return evaluated;
|
||||
|
||||
if (md.getReturnType2() != null) {
|
||||
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return "ENUM_SET:" + String.join(",", values);
|
||||
@@ -110,10 +149,188 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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<>();
|
||||
java.util.Set<String> declaredLocals = new java.util.HashSet<>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
String[] finalResult = new String[1];
|
||||
|
||||
md.getBody().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(vds);
|
||||
}
|
||||
|
||||
@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); // alias without 'this.' only if not shadowed
|
||||
}
|
||||
} else {
|
||||
localVars.put(varName, rhsVal);
|
||||
if (!declaredLocals.contains(varName)) {
|
||||
localVars.put("this." + varName, rhsVal); // alias with 'this.' only if not shadowed
|
||||
}
|
||||
}
|
||||
}
|
||||
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; // Do not visit children — ReturnStatements inside the switch are handled by 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) {
|
||||
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues);
|
||||
if (switchVar == null) return null;
|
||||
|
||||
boolean matchingCase = false;
|
||||
for (Object stmtObj : ss.statements()) {
|
||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
|
||||
matchingCase = false;
|
||||
if (sc.isDefault()) {
|
||||
matchingCase = true;
|
||||
} else {
|
||||
for (Object exprObj : sc.expressions()) {
|
||||
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||
if (caseVal != null && switchVar.endsWith(caseVal)) {
|
||||
matchingCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (matchingCase) {
|
||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
return resolveInternal(rs.getExpression(), context, visited);
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
|
||||
return resolveInternal(ys.getExpression(), context, visited);
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||
return resolveInternal(es.getExpression(), context, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
||||
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues);
|
||||
if (switchVar == null) return null;
|
||||
|
||||
boolean matchingCase = false;
|
||||
for (Object stmtObj : se.statements()) {
|
||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
|
||||
matchingCase = false;
|
||||
if (sc.isDefault()) {
|
||||
matchingCase = true;
|
||||
} else {
|
||||
for (Object exprObj : sc.expressions()) {
|
||||
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||
if (caseVal != null && switchVar.endsWith(caseVal)) {
|
||||
matchingCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (matchingCase) {
|
||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||
return resolveInternal(es.getExpression(), context, visited);
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
|
||||
return resolveInternal(ys.getExpression(), context, visited);
|
||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
return resolveInternal(rs.getExpression(), context, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveExpressionWithParams(Expression expr, java.util.Map<String, String> paramValues) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String name = sn.getIdentifier();
|
||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||
return null;
|
||||
} else if (expr instanceof FieldAccess fa) {
|
||||
String name = "this." + fa.getName().getIdentifier();
|
||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||
return null;
|
||||
} else if (expr instanceof QualifiedName qn) {
|
||||
return qn.getName().getIdentifier();
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
|
||||
return sl.getLiteralValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||
|
||||
@@ -193,6 +410,28 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check constructors for assignments
|
||||
for (MethodDeclaration md : td.getMethods()) {
|
||||
if (md.isConstructor() && md.getBody() != null) {
|
||||
for (Object stmtObj : md.getBody().statements()) {
|
||||
if (stmtObj instanceof ExpressionStatement es) {
|
||||
if (es.getExpression() instanceof Assignment assignment) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
boolean matches = false;
|
||||
if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(fieldName)) {
|
||||
matches = true;
|
||||
} else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName) && fa.getExpression() instanceof ThisExpression) {
|
||||
matches = true;
|
||||
}
|
||||
if (matches) {
|
||||
return resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
visited.remove(fieldId);
|
||||
}
|
||||
@@ -238,4 +477,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||
System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
|
||||
tracedVar = extractedFinalTraced[0];
|
||||
@@ -174,10 +175,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
// Parse resolvedValue using JDT to robustly handle complex expressions
|
||||
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest());
|
||||
exprParser.setSource(resolvedValue.toCharArray());
|
||||
System.out.println("DEBUG Parsing resolvedValue: " + resolvedValue);
|
||||
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
|
||||
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
|
||||
exprParser.setSource(resolvedValue.toCharArray());
|
||||
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
|
||||
System.out.println("DEBUG exprNode: " + (exprNode != null ? exprNode.getClass().getName() : "null") + ", node: " + exprNode);
|
||||
|
||||
String varName = null;
|
||||
String methodName = null;
|
||||
@@ -186,6 +189,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
methodName = mi.getName().getIdentifier();
|
||||
System.out.println("DEBUG mi.getName(): " + methodName + ", mi.getExpression() type: " + (mi.getExpression() != null ? mi.getExpression().getClass().getName() : "null"));
|
||||
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
|
||||
varName = sn.getIdentifier();
|
||||
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
|
||||
@@ -195,11 +199,44 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
declaredType = ce.getType().toString();
|
||||
sourceMethod = "inline-cast";
|
||||
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
System.out.println("DEBUG found ClassInstanceCreation!");
|
||||
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
sourceMethod = "inline-instantiation";
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
System.out.println("DEBUG resolving anonymous class!");
|
||||
final List<String> polyEventsRef = polymorphicEvents;
|
||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||
if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) {
|
||||
System.out.println("DEBUG visiting anonymous class method: " + methodName);
|
||||
mDecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
if (rs.getExpression() != null) {
|
||||
System.out.println("DEBUG found return expression: " + rs.getExpression().toString());
|
||||
List<org.eclipse.jdt.core.dom.Expression> tracedReturns = traceVariableAll(rs.getExpression());
|
||||
for (org.eclipse.jdt.core.dom.Expression tracedRet : tracedReturns) {
|
||||
extractConstantsFromExpression(tracedRet, polyEventsRef);
|
||||
}
|
||||
}
|
||||
return super.visit(rs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
|
||||
methodName = innerMi.getName().getIdentifier();
|
||||
if (innerMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName snInner) {
|
||||
varName = snInner.getIdentifier();
|
||||
} else {
|
||||
String exprStr = innerMi.getExpression() != null ? innerMi.getExpression().toString() : "";
|
||||
if (!exprStr.contains("(")) {
|
||||
varName = exprStr;
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
@@ -240,10 +277,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())
|
||||
@@ -258,9 +298,77 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
if (varName != null && declaredType == null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -271,11 +379,40 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (staticTd != null) {
|
||||
declaredType = context.getFqn(staticTd);
|
||||
sourceMethod = "static-call";
|
||||
} else if (context.getEnumValues(varName) != null) {
|
||||
declaredType = varName;
|
||||
sourceMethod = "static-call";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
|
||||
System.out.println("DEBUG fallback triggered for methodName: " + methodName);
|
||||
for (String methodFqn : path) {
|
||||
System.out.println("DEBUG fallback checking path method: " + methodFqn);
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) {
|
||||
System.out.println("DEBUG fallback found method in " + context.getFqn(currentTd));
|
||||
CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
List<String> values = resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
|
||||
System.out.println("DEBUG fallback resolveMethodReturnConstant returned: " + values);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
polymorphicEvents.addAll(values);
|
||||
sourceMethod = methodFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (declaredType != null) {
|
||||
List<String> enumValues = context.getEnumValues(declaredType);
|
||||
if (enumValues != null && !enumValues.isEmpty()) {
|
||||
for (String ev : enumValues) {
|
||||
polymorphicEvents.add(parseEnumSetElement(ev));
|
||||
}
|
||||
} else {
|
||||
List<String> typesToInspect = new ArrayList<>();
|
||||
if ("inline-instantiation".equals(sourceMethod)) {
|
||||
typesToInspect.add(declaredType);
|
||||
@@ -307,8 +444,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
|
||||
// for string literals or enum-like constants.
|
||||
}
|
||||
|
||||
|
||||
boolean hasValidConstant = false;
|
||||
for (String ev : polymorphicEvents) {
|
||||
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
|
||||
@@ -318,36 +456,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidConstant && resolvedValue.contains("(")) {
|
||||
java.util.List<String> scraped = new java.util.ArrayList<>();
|
||||
|
||||
// Extract "STRING_LITERALS"
|
||||
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
|
||||
while (m1.find()) {
|
||||
scraped.add(m1.group(1));
|
||||
// As a fallback, attempt direct AST extraction (e.g. from MessageBuilder chains or inline constructor args)
|
||||
if (!hasValidConstant && exprNode instanceof org.eclipse.jdt.core.dom.Expression) {
|
||||
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) exprNode, polymorphicEvents);
|
||||
}
|
||||
|
||||
// Extract ENUM_LIKE_CONSTANTS
|
||||
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
|
||||
while (m2.find()) {
|
||||
if (!scraped.contains(m2.group(1))) {
|
||||
scraped.add(m2.group(1));
|
||||
}
|
||||
}
|
||||
// The aggressive regex scraper has been completely removed to avoid false positives.
|
||||
// We rely on precise AST traversal instead.
|
||||
|
||||
if (!scraped.isEmpty()) {
|
||||
polymorphicEvents.clear();
|
||||
polymorphicEvents.addAll(scraped);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
|
||||
if (polymorphicEvents.size() > 1) {
|
||||
polymorphicEvents.removeIf(e -> {
|
||||
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
|
||||
return !val.equals(val.toUpperCase()) || val.length() <= 2;
|
||||
});
|
||||
}
|
||||
List<String> newPolyEvents = new ArrayList<>();
|
||||
for (String pe : polymorphicEvents) {
|
||||
List<String> resolved = resolveClassConstantReturns(pe, context, null);
|
||||
@@ -359,6 +475,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
polymorphicEvents = newPolyEvents;
|
||||
|
||||
// Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names)
|
||||
polymorphicEvents.removeIf(e -> {
|
||||
if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it
|
||||
String val = e;
|
||||
return !val.equals(val.toUpperCase()) || val.length() <= 1;
|
||||
});
|
||||
|
||||
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||
|
||||
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
@@ -392,36 +517,147 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected String getVariableDeclaredType(String methodFqn, String varName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||
System.out.println("DEBUG getVariableDeclaredType called with: " + methodFqn + ", " + cleanFieldName);
|
||||
if (methodFqn == null) return null;
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
String fqn = methodFqn.substring(0, lastDot);
|
||||
String methodName = methodFqn.substring(lastDot + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||
System.out.println("DEBUG getTypeDeclaration for " + fqn + " = " + (td != null));
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
|
||||
System.out.println("DEBUG findMethodDeclaration for " + methodName + " = " + (md != null));
|
||||
if (md != null) {
|
||||
for (Object pObj : md.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
|
||||
return svd.getType().toString();
|
||||
}
|
||||
}
|
||||
final String[] foundType = new String[1];
|
||||
if (md.getBody() != null) {
|
||||
System.out.println("DEBUG visiting body for " + cleanFieldName);
|
||||
md.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)) {
|
||||
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||
foundType[0] = node.getType().toString();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) {
|
||||
for (Object paramObj : node.parameters()) {
|
||||
org.eclipse.jdt.core.dom.VariableDeclaration param = (org.eclipse.jdt.core.dom.VariableDeclaration) paramObj;
|
||||
System.out.println("DEBUG visiting lambda param: " + param.getName().getIdentifier());
|
||||
if (param.getName().getIdentifier().equals(cleanFieldName)) {
|
||||
System.out.println("DEBUG found lambda param: " + cleanFieldName);
|
||||
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
|
||||
foundType[0] = svd.getType().toString();
|
||||
System.out.println("DEBUG lambda explicit type: " + foundType[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
org.eclipse.jdt.core.dom.ASTNode parent = node.getParent();
|
||||
if (parent instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
org.eclipse.jdt.core.dom.Expression caller = mi.getExpression();
|
||||
|
||||
// Phase 6: Stream API chains
|
||||
boolean hasMap = false;
|
||||
while (caller instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) {
|
||||
String mName = chainMi.getName().getIdentifier();
|
||||
if (mName.equals("map") || mName.equals("flatMap")) {
|
||||
hasMap = true;
|
||||
// Extract return type of the map lambda
|
||||
if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof org.eclipse.jdt.core.dom.LambdaExpression lambda) {
|
||||
if (lambda.getBody() instanceof org.eclipse.jdt.core.dom.MethodInvocation mapMi) {
|
||||
// Just a heuristic for now: we use the method's return type if we could resolve it, but since we can't easily,
|
||||
// we'll try to find the type of the caller and look up the method.
|
||||
org.eclipse.jdt.core.dom.Expression mapCaller = mapMi.getExpression();
|
||||
if (mapCaller instanceof org.eclipse.jdt.core.dom.SimpleName mapSn) {
|
||||
String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
|
||||
if (mapCallerType != null) {
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
|
||||
if (td != null) {
|
||||
MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
|
||||
if (mapMd != null && mapMd.getReturnType2() != null) {
|
||||
foundType[0] = mapMd.getReturnType2().toString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") ||
|
||||
mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") ||
|
||||
mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
|
||||
caller = chainMi.getExpression();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMap) {
|
||||
if (caller instanceof org.eclipse.jdt.core.dom.SimpleName callerSn) {
|
||||
String callerName = callerSn.getIdentifier();
|
||||
System.out.println("DEBUG callerName: " + callerName + " cleanFieldName: " + cleanFieldName);
|
||||
if (!callerName.equals(cleanFieldName)) {
|
||||
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||
System.out.println("DEBUG callerType for " + callerName + " = " + callerType);
|
||||
if (callerType != null && callerType.contains("<")) {
|
||||
int start = callerType.indexOf('<');
|
||||
int end = callerType.lastIndexOf('>');
|
||||
if (start >= 0 && end > start) {
|
||||
foundType[0] = callerType.substring(start + 1, end);
|
||||
System.out.println("DEBUG foundType[0]: " + foundType[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (caller instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
|
||||
String callerName = fa.getName().getIdentifier();
|
||||
if (!callerName.equals(cleanFieldName)) {
|
||||
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||
if (callerType != null && callerType.contains("<")) {
|
||||
int start = callerType.indexOf('<');
|
||||
int end = callerType.lastIndexOf('>');
|
||||
if (start >= 0 && end > start) {
|
||||
foundType[0] = callerType.substring(start + 1, end);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return foundType[0];
|
||||
if (foundType[0] != null) return foundType[0];
|
||||
}
|
||||
|
||||
// Fallback to fields
|
||||
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||
return fd.getType().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -507,15 +743,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||
if (!consts.isEmpty()) {
|
||||
constants.addAll(consts);
|
||||
} else {
|
||||
constants.add(sn.toString());
|
||||
}
|
||||
} else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
|
||||
List<String> consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
|
||||
if (!consts.isEmpty()) {
|
||||
constants.addAll(consts);
|
||||
} else {
|
||||
constants.add(fa.getName().getIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,6 +756,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
if (impls != null) {
|
||||
for (String implName : impls) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
|
||||
constants.addAll(delegationResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visited.remove(fqn);
|
||||
@@ -587,9 +827,38 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return super.visit(rs);
|
||||
}
|
||||
});
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayAccess aa) {
|
||||
extractConstantsFromExpression(aa.getArray(), constants);
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayCreation ac && ac.getInitializer() != null) {
|
||||
for (Object expObj : ac.getInitializer().expressions()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants);
|
||||
}
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayInitializer ai) {
|
||||
for (Object expObj : ai.expressions()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants);
|
||||
}
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
for (Object argObj : cic.arguments()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
|
||||
}
|
||||
} else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
|
||||
// Phase 7: List.get(0) indexing
|
||||
if (methodName.equals("get") && mi.arguments().size() == 1 && mi.getExpression() != null) {
|
||||
extractConstantsFromExpression(mi.getExpression(), constants);
|
||||
return;
|
||||
}
|
||||
// Phase 7: List.of("A", "B") or Arrays.asList("A", "B")
|
||||
if ((methodName.equals("of") || methodName.equals("asList")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName) {
|
||||
for (Object argObj : mi.arguments()) {
|
||||
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 1. Local setter tracking
|
||||
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
@@ -631,7 +900,59 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delegate to method return analysis
|
||||
// 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType())
|
||||
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||
if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mdecl) {
|
||||
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
|
||||
mdecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||
if (rs.getExpression() != null) {
|
||||
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||
}
|
||||
return super.visit(rs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
boolean handledByLombok = false;
|
||||
if (methodName.startsWith("get") && methodName.length() > 3) {
|
||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
|
||||
if (typeDecl != null) {
|
||||
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
|
||||
int fieldIndex = findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
|
||||
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) cic.arguments().get(fieldIndex), constants);
|
||||
handledByLombok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handledByLombok) {
|
||||
for (Object argObj : cic.arguments()) {
|
||||
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Builder Pattern (e.g. MessageBuilder.withPayload(...).setHeader("event", CONSTANT).build())
|
||||
org.eclipse.jdt.core.dom.Expression currentMi = mi;
|
||||
while (currentMi instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) {
|
||||
String chainName = chainMi.getName().getIdentifier();
|
||||
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
|
||||
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(1), constants);
|
||||
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
|
||||
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(0), constants);
|
||||
}
|
||||
currentMi = chainMi.getExpression();
|
||||
}
|
||||
|
||||
// 4. Delegate to method return analysis
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) {
|
||||
List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null);
|
||||
@@ -640,6 +961,72 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
|
||||
boolean isAllArgsConstructor = false;
|
||||
boolean isRequiredArgsConstructor = false;
|
||||
boolean isData = false;
|
||||
boolean isValue = false;
|
||||
|
||||
for (Object modObj : td.modifiers()) {
|
||||
if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) {
|
||||
String name = ma.getTypeName().getFullyQualifiedName();
|
||||
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||
} else if (modObj instanceof org.eclipse.jdt.core.dom.NormalAnnotation na) {
|
||||
String name = na.getTypeName().getFullyQualifiedName();
|
||||
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||
boolean isStatic = false;
|
||||
boolean isFinal = false;
|
||||
boolean hasNonNull = false;
|
||||
for (Object modObj : fd.modifiers()) {
|
||||
if (modObj instanceof org.eclipse.jdt.core.dom.Modifier mod) {
|
||||
if (mod.isStatic()) isStatic = true;
|
||||
if (mod.isFinal()) isFinal = true;
|
||||
} else if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) {
|
||||
if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
|
||||
}
|
||||
}
|
||||
if (isStatic) continue;
|
||||
|
||||
if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||
if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
|
||||
if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) {
|
||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||
if ("String".equals(typeName)) {
|
||||
extractConstantsFromExpression(argObj, constants);
|
||||
}
|
||||
} else {
|
||||
extractConstantsFromExpression(argObj, constants);
|
||||
}
|
||||
}
|
||||
|
||||
protected org.eclipse.jdt.core.dom.Block findEnclosingBlock(org.eclipse.jdt.core.dom.ASTNode node) {
|
||||
org.eclipse.jdt.core.dom.ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof org.eclipse.jdt.core.dom.Block)) {
|
||||
@@ -656,46 +1043,105 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
final Expression[] initializer = new Expression[1];
|
||||
List<Expression> initializers = new ArrayList<>();
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
initializer[0] = node.getInitializer();
|
||||
initializers.add(node.getInitializer());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
initializer[0] = node.getRightHandSide();
|
||||
initializers.add(node.getRightHandSide());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (initializer[0] != null) {
|
||||
Expression expr = traceVariable(initializer[0]);
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
|
||||
if (!initializers.isEmpty()) {
|
||||
List<String> stringified = new ArrayList<>();
|
||||
for (Expression expr : initializers) {
|
||||
Expression traced = traceVariable(expr);
|
||||
if (traced instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
Expression innerMost = unwrapMethodInvocation(mi, 0);
|
||||
if (innerMost instanceof MethodInvocation innerMi) {
|
||||
if (innerMost instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
|
||||
if (innerMi.getExpression() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
|
||||
stringified.add(sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()");
|
||||
} else {
|
||||
stringified.add(innerMi.getName().getIdentifier() + "()");
|
||||
}
|
||||
return innerMi.getName().getIdentifier() + "()";
|
||||
} else if (innerMost instanceof SimpleName sn) {
|
||||
stringified.add(sn.getIdentifier());
|
||||
} else {
|
||||
stringified.add(innerMost.toString());
|
||||
}
|
||||
if (innerMost instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return innerMost.toString();
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return expr.toString();
|
||||
} else if (traced instanceof SimpleName sn) {
|
||||
stringified.add(sn.getIdentifier());
|
||||
} else if (traced instanceof org.eclipse.jdt.core.dom.ArrayInitializer) {
|
||||
stringified.add("new Object[]" + traced.toString());
|
||||
} else {
|
||||
stringified.add(traced.toString());
|
||||
}
|
||||
}
|
||||
if (stringified.size() == 1) return stringified.get(0);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < stringified.size() - 1; i++) {
|
||||
sb.append("true ? ").append(stringified.get(i)).append(" : ");
|
||||
}
|
||||
sb.append(stringified.get(stringified.size() - 1));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// If local variable not found, check field assignments
|
||||
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
|
||||
final Expression[] fieldInitializer = new Expression[1];
|
||||
ASTVisitor fieldVisitor = new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
|
||||
fieldInitializer[0] = node.getInitializer();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
Expression left = node.getLeftHandSide();
|
||||
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
|
||||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
|
||||
fieldInitializer[0] = node.getRightHandSide();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
};
|
||||
|
||||
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
|
||||
fieldInitializer[0] = frag.getInitializer();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||
|
||||
for (MethodDeclaration classMd : td.getMethods()) {
|
||||
if (classMd.isConstructor() && classMd.getBody() != null) {
|
||||
classMd.getBody().accept(fieldVisitor);
|
||||
}
|
||||
}
|
||||
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||
|
||||
for (MethodDeclaration classMd : td.getMethods()) {
|
||||
if (!classMd.isConstructor() && classMd.getBody() != null) {
|
||||
classMd.getBody().accept(fieldVisitor);
|
||||
}
|
||||
}
|
||||
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -746,11 +1192,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||
protected Expression unwrapMethodInvocation(org.eclipse.jdt.core.dom.MethodInvocation mi, int depth) {
|
||||
if (depth > 5) return mi;
|
||||
// Do not unwrap if called on an object (e.g. mapper.toEvent)
|
||||
// Exceptions: Optional.of, Optional.ofNullable, Stream, etc.
|
||||
if (mi.getExpression() != null) {
|
||||
String exprStr = mi.getExpression().toString();
|
||||
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
|
||||
return mi;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
String mName = mi.getName().getIdentifier();
|
||||
String lowerName = mName.toLowerCase();
|
||||
|
||||
// Do not unwrap methods that explicitly convert, map, parse, or create
|
||||
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||
lowerName.startsWith("find")) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1);
|
||||
}
|
||||
return arg;
|
||||
@@ -792,10 +1263,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
|
||||
if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
|
||||
int bracketIndex = paramName.indexOf('[');
|
||||
int dotIndex = paramName.indexOf('.');
|
||||
if (dotIndex > 0 && dotIndex + 1 < paramName.length() && Character.isLowerCase(paramName.charAt(dotIndex + 1))) {
|
||||
|
||||
if (bracketIndex > 0 && (dotIndex == -1 || bracketIndex < dotIndex)) {
|
||||
return new String[] { paramName.substring(0, bracketIndex), paramName.substring(bracketIndex) + currentSuffix };
|
||||
} else if (dotIndex > 0) {
|
||||
if (paramName.startsWith("this.")) {
|
||||
int nextDotIndex = paramName.indexOf('.', 5);
|
||||
if (nextDotIndex > 0) {
|
||||
return new String[] { paramName.substring(0, nextDotIndex), paramName.substring(nextDotIndex) + currentSuffix };
|
||||
}
|
||||
return new String[] { paramName, currentSuffix };
|
||||
}
|
||||
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
|
||||
}
|
||||
}
|
||||
return new String[] { paramName, currentSuffix };
|
||||
}
|
||||
|
||||
@@ -849,36 +1333,88 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
protected Expression traceVariable(Expression expr) {
|
||||
List<Expression> all = traceVariableAll(expr);
|
||||
List<Expression> all = traceVariableAll(expr, new HashSet<>());
|
||||
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||
}
|
||||
|
||||
protected List<Expression> traceVariableAll(Expression expr) {
|
||||
return traceVariableAll(expr, new HashSet<>());
|
||||
}
|
||||
|
||||
protected List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
|
||||
List<Expression> results = new ArrayList<>();
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
if (!visitedVariables.add(varName)) {
|
||||
return results; // Break infinite loop
|
||||
}
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||
if (enclosingMethod != null) {
|
||||
enclosingMethod.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
results.addAll(traceVariableAll(node.getInitializer()));
|
||||
results.addAll(traceVariableAll(node.getInitializer(), visitedVariables));
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
results.addAll(traceVariableAll(node.getRightHandSide()));
|
||||
results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables));
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (!results.isEmpty()) {
|
||||
visitedVariables.remove(varName);
|
||||
return results;
|
||||
}
|
||||
|
||||
// Tracing parameter callers (Inter-procedural within the same class)
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj;
|
||||
if (param.getName().getIdentifier().equals(varName)) {
|
||||
int paramIndex = enclosingMethod.parameters().indexOf(param);
|
||||
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
|
||||
if (enclosingType != null) {
|
||||
String mName = enclosingMethod.getName().getIdentifier();
|
||||
List<Expression> callerExprs = new ArrayList<>();
|
||||
enclosingType.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) {
|
||||
if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) {
|
||||
callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables));
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ExpressionMethodReference node) {
|
||||
if (node.getName().getIdentifier().equals(mName)) {
|
||||
if (node.getParent() instanceof org.eclipse.jdt.core.dom.MethodInvocation parentMi) {
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) {
|
||||
callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables));
|
||||
} else {
|
||||
for (Object arg : parentMi.arguments()) {
|
||||
if (arg != node) {
|
||||
callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (!callerExprs.isEmpty()) {
|
||||
visitedVariables.remove(varName);
|
||||
return callerExprs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visitedVariables.remove(varName);
|
||||
}
|
||||
results.add(expr);
|
||||
return results;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,7 +45,13 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
if (refMethod != null) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -58,7 +64,13 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
@@ -141,11 +153,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||
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
|
||||
}
|
||||
// Removed eager tracing here to allow dynamic tracing in resolveTriggerPointParameters
|
||||
|
||||
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
||||
Expression firstArg = (Expression) cic.arguments().get(0);
|
||||
@@ -161,6 +169,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
System.out.println("DEBUG resolveArguments: " + args);
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
if (refMethod != null) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -62,7 +68,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
@@ -146,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
|
||||
@@ -357,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,62 @@ public final class AstUtils {
|
||||
return type.toString(); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
|
||||
if (node.getExpression() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
|
||||
if (typeBinding != null) {
|
||||
if (isCollectionOrStreamType(typeBinding)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String methodName = node.getName().getIdentifier();
|
||||
if (methodName.equals("forEach") || methodName.equals("map") ||
|
||||
methodName.equals("filter") || methodName.equals("flatMap") ||
|
||||
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
|
||||
methodName.equals("noneMatch") || methodName.equals("reduce") ||
|
||||
methodName.equals("collect") || methodName.equals("removeIf") ||
|
||||
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
|
||||
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
|
||||
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
|
||||
methodName.equals("merge") || methodName.equals("peek") ||
|
||||
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
|
||||
methodName.equals("doOnError") || methodName.equals("concatMap") ||
|
||||
methodName.equals("switchMap")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
|
||||
String fqn = typeBinding.getErasure().getQualifiedName();
|
||||
if (fqn.startsWith("java.util.stream.") ||
|
||||
fqn.equals("java.lang.Iterable") ||
|
||||
fqn.equals("java.util.Collection") ||
|
||||
fqn.equals("java.util.List") ||
|
||||
fqn.equals("java.util.Set") ||
|
||||
fqn.equals("java.util.Map") ||
|
||||
fqn.equals("java.util.Iterator") ||
|
||||
fqn.equals("java.util.Optional") ||
|
||||
fqn.equals("reactor.core.publisher.Flux") ||
|
||||
fqn.equals("reactor.core.publisher.Mono")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||
if (isCollectionOrStreamType(intf)) return true;
|
||||
}
|
||||
|
||||
if (typeBinding.getSuperclass() != null) {
|
||||
return isCollectionOrStreamType(typeBinding.getSuperclass());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class CodebaseContext {
|
||||
return Collections.unmodifiableList(libraryHints);
|
||||
}
|
||||
|
||||
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**"));
|
||||
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
|
||||
private String[] classpath = new String[0];
|
||||
private String[] sourcepath = new String[0];
|
||||
private boolean resolveBindings = false;
|
||||
@@ -191,6 +191,8 @@ public class CodebaseContext {
|
||||
indexType(td, packageName, javaFile);
|
||||
} else if (type instanceof EnumDeclaration ed) {
|
||||
indexEnum(ed, packageName, javaFile);
|
||||
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
|
||||
indexRecord(rd, packageName, javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,6 +237,33 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
||||
String simpleName = rd.getName().getIdentifier();
|
||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||
|
||||
// Treat as a class since it is a type
|
||||
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
||||
classPaths.put(fqn, javaFile);
|
||||
|
||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||
if (!existingFqn.equals(fqn)) {
|
||||
ambiguousSimpleNames.add(simpleName);
|
||||
}
|
||||
} else {
|
||||
simpleNameToFqn.put(simpleName, fqn);
|
||||
}
|
||||
|
||||
// Recursively index nested types
|
||||
for (Object type : rd.bodyDeclarations()) {
|
||||
if (type instanceof TypeDeclaration nestedTd) {
|
||||
indexType(nestedTd, fqn, javaFile);
|
||||
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
|
||||
indexRecord(nestedRd, fqn, javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getImplementations(String interfaceName) {
|
||||
Set<String> allImpls = new HashSet<>();
|
||||
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||
|
||||
@@ -45,6 +45,7 @@ public class ExportService {
|
||||
new EntryPointEnricher(),
|
||||
new PropertyEnricher(),
|
||||
new CallChainEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -152,4 +152,334 @@ public class ConstantResolverTest {
|
||||
String result = resolver.resolve(returnExpr, context);
|
||||
assertThat(result).isEqualTo("${app.path}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorAssignmentAndFieldAccess(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("EventDto.java"),
|
||||
"package com.example;\n" +
|
||||
"public class EventDto {\n" +
|
||||
" private String eventType;\n" +
|
||||
" public EventDto() {\n" +
|
||||
" this.eventType = \"PAYMENT_SUCCESS\";\n" +
|
||||
" }\n" +
|
||||
" public String getEventType() {\n" +
|
||||
" return this.eventType;\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.EventDto");
|
||||
MethodDeclaration method = td.getMethods()[1]; // getEventType
|
||||
ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
|
||||
Expression returnExpr = rs.getExpression(); // this.eventType
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(returnExpr, context);
|
||||
|
||||
assertThat(result).isEqualTo("PAYMENT_SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMethodEvaluationWithNestedBlocksAndVars(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Logic.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Logic {\n" +
|
||||
" public String process(String event) {\n" +
|
||||
" String myVar = event;\n" +
|
||||
" if (myVar != null) {\n" +
|
||||
" return myVar;\n" +
|
||||
" }\n" +
|
||||
" return \"DEFAULT\";\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Logic logic = new Logic();\n" +
|
||||
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process("MY_EVENT")
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
assertThat(result).isEqualTo("MY_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMethodShadowingReturnsParameter(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Logic.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Logic {\n" +
|
||||
" public String a = \"OLD_FIELD\";\n" +
|
||||
" public String process(String a) {\n" + // shadowing
|
||||
" this.a = \"NEW_FIELD\";\n" +
|
||||
" return a;\n" + // should return the parameter
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Logic logic = new Logic();\n" +
|
||||
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
assertThat(result).isEqualTo("PARAM_VALUE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMethodShadowingReturnsField(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Logic.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Logic {\n" +
|
||||
" public String a = \"OLD_FIELD\";\n" +
|
||||
" public String process(String a) {\n" + // shadowing
|
||||
" this.a = \"NEW_FIELD_VALUE\";\n" +
|
||||
" return this.a;\n" + // should return the field
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Logic logic = new Logic();\n" +
|
||||
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
assertThat(result).isEqualTo("NEW_FIELD_VALUE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMethodImplicitFieldAssignment(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Logic.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Logic {\n" +
|
||||
" public String a = \"OLD_FIELD\";\n" +
|
||||
" public String process(String p) {\n" +
|
||||
" a = p;\n" + // implicit field assignment
|
||||
" return this.a;\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Logic logic = new Logic();\n" +
|
||||
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
assertThat(result).isEqualTo("MY_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZeroArgMethodEvaluation(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Logic.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Logic {\n" +
|
||||
" public String a = \"MY_CONSTANT\";\n" +
|
||||
" public String getEvent() {\n" +
|
||||
" return this.a;\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Logic logic = new Logic();\n" +
|
||||
" String result = logic.getEvent();\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.getEvent()
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
assertThat(result).isEqualTo("MY_CONSTANT");
|
||||
}
|
||||
|
||||
/**
|
||||
* When a switch method is called with a <em>known</em> constant argument, {@code evaluateMethodOutput}
|
||||
* should evaluate the switch and return only the matching branch.
|
||||
*
|
||||
* <p>Regression guard: before the {@code SwitchStatement.visit} fix, the ASTVisitor would also descend
|
||||
* into child {@code ReturnStatement} nodes after the switch was already handled, potentially overwriting
|
||||
* a correct result with a wrong one.
|
||||
*/
|
||||
@Test
|
||||
void testOldStyleSwitchResolvesCorrectBranchForKnownConstant(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Classifier.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Classifier {\n" +
|
||||
" public OrderEvents classify(String q) {\n" +
|
||||
" switch (q) {\n" +
|
||||
" case \"a\": return OrderEvents.A8;\n" +
|
||||
" case \"b\": return OrderEvents.B2;\n" +
|
||||
" default: return OrderEvents.DEF;\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}\n" +
|
||||
"enum OrderEvents { A8, B2, DEF }");
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call() {\n" +
|
||||
" Classifier c = new Classifier();\n" +
|
||||
" OrderEvents r = c.classify(\"a\");\n" + // known constant → should match case "a"
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify("a")
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
// Only the matching branch should be returned, not all branches
|
||||
assertThat(result).isEqualTo("OrderEvents.A8");
|
||||
}
|
||||
|
||||
/**
|
||||
* When a switch method is called with a <em>runtime</em> variable that cannot be statically resolved,
|
||||
* {@code evaluateMethodOutput} must return {@code null} so the resolver falls back to the return-type
|
||||
* enum set (covering all branches).
|
||||
*
|
||||
* <p>Regression: before the fix, {@code SwitchStatement.visit} returned {@code super.visit(ss)} (true),
|
||||
* causing child {@code ReturnStatement} nodes inside the switch to fire. The first return — e.g.
|
||||
* {@code return OrderEvents.A8} — was captured as if it were the only possible result, silently
|
||||
* dropping the other branches. After the fix the visitor returns {@code false}, preventing child
|
||||
* traversal, so the resolver correctly falls back to the full ENUM_SET.
|
||||
*/
|
||||
@Test
|
||||
void testOldStyleSwitchWithUnknownVariableReturnsFallbackEnumSet(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Classifier.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Classifier {\n" +
|
||||
" public OrderEvents classify(String q) {\n" +
|
||||
" switch (q) {\n" +
|
||||
" case \"a\": return OrderEvents.A8;\n" +
|
||||
" case \"b\": return OrderEvents.B2;\n" +
|
||||
" default: return OrderEvents.DEF;\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}\n" +
|
||||
"enum OrderEvents { A8, B2, DEF }");
|
||||
Files.writeString(dir.resolve("Caller.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Caller {\n" +
|
||||
" public void call(String x) {\n" + // x is a runtime parameter — not a constant
|
||||
" Classifier c = new Classifier();\n" +
|
||||
" OrderEvents r = c.classify(x);\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify(x)
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(mi, context);
|
||||
|
||||
// Must cover ALL branches via ENUM_SET — not just the first one ("A8")
|
||||
assertThat(result).startsWith("ENUM_SET:");
|
||||
assertThat(result).contains("OrderEvents.A8");
|
||||
assertThat(result).contains("OrderEvents.B2");
|
||||
assertThat(result).contains("OrderEvents.DEF");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class AnonymousClassTests {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromAnonymousClass() throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class AnonymousService {
|
||||
private EventService service;
|
||||
|
||||
public void process() {
|
||||
MyEvent event = new MyEvent() {
|
||||
@Override
|
||||
public String getEvent() {
|
||||
return "ANON_EVENT";
|
||||
}
|
||||
};
|
||||
service.trigger(event.getEvent());
|
||||
}
|
||||
}
|
||||
class EventService {
|
||||
public void trigger(String ev) {}
|
||||
}
|
||||
class MyEvent {
|
||||
public String getEvent() { return "BASE_EVENT"; }
|
||||
}
|
||||
""";
|
||||
|
||||
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.EventService")
|
||||
.methodName("trigger")
|
||||
.sourceFile("AnonymousService.java")
|
||||
.sourceModule("com.example")
|
||||
.event("event.getEvent()")
|
||||
.lineNumber(12)
|
||||
.build();
|
||||
|
||||
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||
|
||||
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CollectionIndexingTests {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromArrayAccess() throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ArrayService {
|
||||
private EventService service;
|
||||
|
||||
public void process() {
|
||||
String[] events = new String[] {"EVENT_1", "EVENT_2"};
|
||||
service.trigger(events[0]);
|
||||
}
|
||||
}
|
||||
class EventService {
|
||||
public void trigger(String ev) {}
|
||||
}
|
||||
""";
|
||||
|
||||
Files.writeString(tempDir.resolve("ArrayService.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.EventService")
|
||||
.methodName("trigger")
|
||||
.sourceFile("ArrayService.java")
|
||||
.sourceModule("com.example")
|
||||
.event("events[0]")
|
||||
.lineNumber(7)
|
||||
.build();
|
||||
|
||||
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||
|
||||
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
|
||||
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,8 @@ public class GenericEventDetectorTest {
|
||||
"import org.springframework.statemachine.StateMachine;\n" +
|
||||
"public class MyService {\n" +
|
||||
" private StateMachine<String, String> stateMachine;\n" +
|
||||
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
||||
" public MyEnum getEvent() { return someExternalCall(); }\n" +
|
||||
" public MyEnum someExternalCall() { return null; }\n" +
|
||||
" public void trigger() {\n" +
|
||||
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||
" }\n" +
|
||||
|
||||
@@ -1002,9 +1002,10 @@ class HeuristicCallGraphEngineTest {
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
CallChain chain = chains.get(0);
|
||||
// It successfully traces the local variable 'event' to its anonymous class initializer
|
||||
assertThat(chain.getTriggerPoint().getEvent()).startsWith("new RichEvent(){");
|
||||
assertThat(chain.getTriggerPoint().getEvent()).endsWith(".getType()");
|
||||
// The engine traces the anonymous class's getType() through the RichEvent interface,
|
||||
// resolving to OrderEvents.CREATE via the enum return-type fallback.
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("OrderEvents.CREATE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1097,7 +1098,7 @@ class HeuristicCallGraphEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException {
|
||||
void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
@@ -1142,9 +1143,69 @@ class HeuristicCallGraphEngineTest {
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
CallChain chain = chains.get(0);
|
||||
// The ASTVisitor picks up Assignments in textual order.
|
||||
// CANCELLED is the last one textually in the method.
|
||||
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED");
|
||||
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
private OrderMapper mapper = new OrderMapperImpl();
|
||||
public void processOrderEvent(String eventDtoStr) {
|
||||
OrderEvent e = mapper.toEvent(eventDtoStr);
|
||||
service.updateOrderState(e.getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void updateOrderState(String event) {}
|
||||
}
|
||||
|
||||
interface OrderMapper {
|
||||
OrderEvent toEvent(String source);
|
||||
}
|
||||
|
||||
class OrderMapperImpl implements OrderMapper {
|
||||
@Override
|
||||
public OrderEvent toEvent(String source) {
|
||||
return new OrderEvent("MAPPED_EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
class OrderEvent {
|
||||
private String event;
|
||||
public OrderEvent(String event) { this.event = event; }
|
||||
public String getEvent() { return event; }
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.OrderController")
|
||||
.methodName("processOrderEvent")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("updateOrderState")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
CallChain chain = chains.get(0);
|
||||
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1190,7 +1251,7 @@ class HeuristicCallGraphEngineTest {
|
||||
CallChain chain = chains.get(0);
|
||||
// We don't dynamically resolve array indexes, but it shouldn't crash.
|
||||
// It returns the array access expression as a string.
|
||||
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]");
|
||||
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2134,5 +2195,639 @@ class HeuristicCallGraphEngineTest {
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromConstructorAssignment() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
|
||||
service.process(e.getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithConstructorArg {
|
||||
private String type;
|
||||
public EventWithConstructorArg(String typeArg, int other) {
|
||||
this.type = typeArg;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_CONSTRUCTOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrioritizeSetterOverConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
|
||||
e.setType("EVENT_FROM_SETTER");
|
||||
service.process(e.getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithConstructorArg {
|
||||
private String type;
|
||||
public EventWithConstructorArg(String typeArg, int other) {
|
||||
this.type = typeArg;
|
||||
}
|
||||
public void setType(String t) { this.type = t; }
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromArrayAndList() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.List;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleArray() {
|
||||
String[] events = {"ARRAY_EVENT"};
|
||||
service.process(events[0]);
|
||||
}
|
||||
public void handleList() {
|
||||
List<String> listEvents = List.of("LIST_EVENT");
|
||||
service.process(listEvents.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_array");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
|
||||
EntryPoint entryPoint2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
|
||||
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
|
||||
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
private EventWithConstructorArg e;
|
||||
public OrderController() {
|
||||
this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
|
||||
}
|
||||
public void handleStatus() {
|
||||
service.process(this.e.getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithConstructorArg {
|
||||
private String type;
|
||||
public EventWithConstructorArg(String typeArg, int other) {
|
||||
this.type = typeArg;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
System.out.println("DEBUG CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromLambda() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.List;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleList(List<LambdaEvent> events) {
|
||||
events.forEach(e -> service.process(e.getType()));
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaEvent {
|
||||
private String type;
|
||||
public LambdaEvent(String typeArg) { this.type = typeArg; }
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_lambda");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
System.out.println("DEBUG LAMBDA CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromAnonymousClass() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new EventProvider() {
|
||||
@Override
|
||||
public String getEvent() { return "ANONYMOUS_EVENT"; }
|
||||
}.getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
interface EventProvider {
|
||||
String getEvent();
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_anonymous");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ANONYMOUS_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromBranchingAssignments() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(boolean isRefund) {
|
||||
String event;
|
||||
if (isRefund) {
|
||||
event = "REFUND_EVENT";
|
||||
} else {
|
||||
event = "CHARGE_EVENT";
|
||||
}
|
||||
service.process(event);
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_branch");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
|
||||
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("REFUND_EVENT", "CHARGE_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import lombok.Data;
|
||||
import lombok.AllArgsConstructor;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleLombok() {
|
||||
LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
|
||||
service.process(e.getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
class LombokEvent {
|
||||
private String event;
|
||||
private int id;
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_lombok");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("LOMBOK_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromMethodReference() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.List;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleMethodRef() {
|
||||
List<RefEvent> events = List.of(new RefEvent("METHOD_REF_EVENT"));
|
||||
events.forEach(this::delegateProcessing);
|
||||
}
|
||||
private void delegateProcessing(RefEvent e) {
|
||||
service.process(e.getType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
class RefEvent {
|
||||
private String type;
|
||||
public RefEvent(String type) { this.type = type; }
|
||||
public String getType() { return type; }
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_method_ref");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleMethodRef").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("METHOD_REF_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromJavaRecord() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleRecord() {
|
||||
RecordEvent e = new RecordEvent("RECORD_EVENT", 42);
|
||||
service.process(e.type());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
record RecordEvent(String type, int someId) {}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_record");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleRecord").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("RECORD_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import lombok.Data;
|
||||
import lombok.AllArgsConstructor;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleLombok() {
|
||||
MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
|
||||
service.process(e.getEventType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
class MultiEvent {
|
||||
private String eventType;
|
||||
private String unrelatedString;
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExtractConstantsFromNestedClassInstanceCreations() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleNestedConstructors() {
|
||||
// The event contains a nested constructor. The heuristic should NOT extract WRONG_EVENT.
|
||||
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload("WRONG_EVENT"));
|
||||
service.process(e.getEventType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
class OuterEvent {
|
||||
private String eventType;
|
||||
private InnerPayload payload;
|
||||
public OuterEvent(String eventType, InnerPayload payload) {
|
||||
this.eventType = eventType;
|
||||
this.payload = payload;
|
||||
}
|
||||
public String getEventType() { return eventType; }
|
||||
}
|
||||
|
||||
class InnerPayload {
|
||||
private String someData;
|
||||
public InnerPayload(String someData) { this.someData = someData; }
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_nested_constructor");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedConstructors").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("CORRECT_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExtractConstantsFromNestedArraysOfObjects() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleNestedArrays() {
|
||||
// The event contains a nested array of objects. The heuristic should NOT extract WRONG_EVENT.
|
||||
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload[] { new InnerPayload("WRONG_EVENT") });
|
||||
service.process(e.getEventType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
class OuterEvent {
|
||||
private String eventType;
|
||||
private InnerPayload[] payloads;
|
||||
public OuterEvent(String eventType, InnerPayload[] payloads) {
|
||||
this.eventType = eventType;
|
||||
this.payloads = payloads;
|
||||
}
|
||||
public String getEventType() { return eventType; }
|
||||
}
|
||||
|
||||
class InnerPayload {
|
||||
private String someData;
|
||||
public InnerPayload(String someData) { this.someData = someData; }
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_nested_array");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedArrays").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("CORRECT_EVENT");
|
||||
}
|
||||
@Test
|
||||
void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Endpoint {
|
||||
private OrderService service;
|
||||
public void trigger(String eventString) {
|
||||
service.doSomething(eventString);
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void doSomething(String str) {
|
||||
MyEvents event = mapToEnum(str);
|
||||
sendEvent(event);
|
||||
}
|
||||
private MyEvents mapToEnum(String str) {
|
||||
return MyEvents.valueOf(str);
|
||||
}
|
||||
public void sendEvent(MyEvents e) {}
|
||||
}
|
||||
|
||||
enum MyEvents { A, B }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("Endpoint.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.Endpoint")
|
||||
.methodName("trigger")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
CallChain chain = chains.get(0);
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for the early-exit bug in {@code resolveTriggerPointParameters}.
|
||||
*
|
||||
* <p>When {@code traceLocalSetter} matches a field method call whose name happens to equal the
|
||||
* accessor suffix (e.g. {@code mapper.transform(dto)} matching {@code methodName="transform"}),
|
||||
* and the argument passed to that method is a non-constant runtime parameter, no events are
|
||||
* extracted. The old condition
|
||||
* <pre>if (!polymorphicEvents.isEmpty() || !resolvedValue.equals(tp.getEvent()))</pre>
|
||||
* caused an early return with empty {@code polymorphicEvents} because {@code resolvedValue} had
|
||||
* already diverged from the original event name.
|
||||
*
|
||||
* <p>After the fix the condition is just {@code !polymorphicEvents.isEmpty()}, so resolution
|
||||
* continues through {@code getVariableDeclaredType} → {@code resolveMethodReturnConstant},
|
||||
* which correctly traces the field's declared type to its implementation and extracts the
|
||||
* constant returned by {@code TransformerImpl.transform()}.
|
||||
*/
|
||||
@Test
|
||||
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private Transformer transformer;
|
||||
public void processEvent(String dto) {
|
||||
Event e = transformer.transform(dto);
|
||||
machine.fire(e.getType());
|
||||
}
|
||||
}
|
||||
|
||||
interface Transformer {
|
||||
Event transform(String input);
|
||||
}
|
||||
|
||||
class TransformerImpl implements Transformer {
|
||||
public Event transform(String input) {
|
||||
return new Event("ORDER_DISPATCHED");
|
||||
}
|
||||
}
|
||||
|
||||
class Event {
|
||||
private String type;
|
||||
public Event(String type) { this.type = type; }
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class Machine {
|
||||
public void fire(String event) {}
|
||||
}
|
||||
""";
|
||||
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
|
||||
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.OrderController")
|
||||
.methodName("processEvent")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.Machine")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
|
||||
// NOT return empty polymorphicEvents due to early exit on the setter match.
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class StreamApiTests {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldHandleStreamApiChains() throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.List;
|
||||
|
||||
public class StreamService {
|
||||
private EventService service;
|
||||
|
||||
public void process(List<MyEvent> events) {
|
||||
events.stream()
|
||||
.filter(e -> e.isValid())
|
||||
.map(e -> e.toEvent())
|
||||
.forEach(t -> service.trigger(t.getEvent()));
|
||||
}
|
||||
}
|
||||
class EventService {
|
||||
public void trigger(String ev) {}
|
||||
}
|
||||
class MyEvent {
|
||||
public boolean isValid() { return true; }
|
||||
public OrderEvent toEvent() { return new OrderEvent(); }
|
||||
}
|
||||
class OrderEvent {
|
||||
public String getEvent() { return "STREAM_EVENT"; }
|
||||
}
|
||||
""";
|
||||
|
||||
Files.writeString(tempDir.resolve("StreamService.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.EventService")
|
||||
.methodName("trigger")
|
||||
.sourceFile("StreamService.java")
|
||||
.sourceModule("com.example")
|
||||
.event("t.getEvent()")
|
||||
.lineNumber(11)
|
||||
.build();
|
||||
|
||||
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||
System.out.println("TEST RESULT: " + result);
|
||||
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
|
||||
|
||||
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
|
||||
}
|
||||
}
|
||||
@@ -381,10 +381,18 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.PAID",
|
||||
"event" : "OrderEvents.PAY"
|
||||
}, {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.CANCELED",
|
||||
"event" : "OrderEvents.CANCEL"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -400,7 +408,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "event",
|
||||
"event" : "new PayEvent()",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
"methodName" : "processEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||
@@ -408,25 +416,13 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.PAID",
|
||||
"event" : "OrderEvents.PAY"
|
||||
}, {
|
||||
"sourceState" : "OrderStates.PAID",
|
||||
"targetState" : "OrderStates.FULFILLED",
|
||||
"event" : "OrderEvents.FULFILL"
|
||||
}, {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.CANCELED",
|
||||
"event" : "OrderEvents.CANCEL"
|
||||
}, {
|
||||
"sourceState" : "OrderStates.PAID",
|
||||
"targetState" : "OrderStates.CANCELED",
|
||||
"event" : "OrderEvents.ABCD"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -175,7 +175,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -206,7 +206,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -241,7 +241,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -276,7 +276,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -311,7 +311,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -346,7 +346,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
Reference in New Issue
Block a user