2 Commits

5 changed files with 818 additions and 72 deletions

View File

@@ -63,14 +63,9 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
String methodSuffix = ""; String methodSuffix = "";
// Extract method calls like .getType() so we can trace the base parameter // Extract method calls like .getType() so we can trace the base parameter
int dotIndex = currentParamName.indexOf('.'); String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) { currentParamName = extractedEntry[0];
char nextChar = currentParamName.charAt(dotIndex + 1); methodSuffix = extractedEntry[1];
if (Character.isLowerCase(nextChar)) {
methodSuffix = currentParamName.substring(dotIndex);
currentParamName = currentParamName.substring(0, dotIndex);
}
}
// Walk backwards up the call chain // Walk backwards up the call chain
for (int i = path.size() - 1; i > 0; i--) { for (int i = path.size() - 1; i > 0; i--) {
@@ -84,11 +79,9 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
String tracedVar = traceLocalVariable(target, currentParamName); String tracedVar = traceLocalVariable(target, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) { if (tracedVar != null && !tracedVar.equals(currentParamName)) {
// Extract method calls like .getType() from the traced variable // Extract method calls like .getType() from the traced variable
int dotIdx = tracedVar.indexOf('.'); String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix);
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) { tracedVar = extractedTraced[0];
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix; methodSuffix = extractedTraced[1];
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar; currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix; resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName); paramIndex = getParameterIndex(target, currentParamName);
@@ -108,11 +101,9 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
String arg = edge.getArguments().get(paramIndex); String arg = edge.getArguments().get(paramIndex);
if (arg != null) { if (arg != null) {
// If the argument passed has a method call, extract it // If the argument passed has a method call, extract it
int dotIdx = arg.indexOf('.'); String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) { arg = extractedArg[0];
methodSuffix = arg.substring(dotIdx) + methodSuffix; methodSuffix = extractedArg[1];
arg = arg.substring(0, dotIdx);
}
currentParamName = arg; currentParamName = arg;
resolvedValue = arg + methodSuffix; resolvedValue = arg + methodSuffix;
found = true; found = true;
@@ -154,11 +145,9 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
String tracedVar = traceLocalVariable(entryMethod, currentParamName); String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) { if (tracedVar != null && !tracedVar.equals(currentParamName)) {
int dotIdx = tracedVar.indexOf('.'); String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) { tracedVar = extractedFinalTraced[0];
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix; methodSuffix = extractedFinalTraced[1];
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar; currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix; resolvedValue = tracedVar + methodSuffix;
} }
@@ -166,6 +155,24 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
List<String> polymorphicEvents = new ArrayList<>(); List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.startsWith("ENUM_SET:")) {
for (String eVal : resolvedValue.substring(9).split(",")) {
String parsed = parseEnumSetElement(eVal);
if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
}
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
// Parse resolvedValue using JDT to robustly handle complex expressions // 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()); 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()); exprParser.setSource(resolvedValue.toCharArray());
@@ -461,6 +468,25 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName); if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
String called = superFqn + "." + smi.getName().getIdentifier();
if (visited.contains(called)) {
handled = true;
} else {
String cName = superFqn;
String mName = smi.getName().getIdentifier();
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult); constants.addAll(delegationResult);
@@ -475,7 +501,7 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
if (val != null) { if (val != null) {
if (val.startsWith("ENUM_SET:")) { if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) { for (String eVal : val.substring(9).split(",")) {
String parsed = eVal.substring(eVal.lastIndexOf('.') + 1); String parsed = parseEnumSetElement(eVal);
if (!constants.contains(parsed)) constants.add(parsed); if (!constants.contains(parsed)) constants.add(parsed);
} }
} else { } else {
@@ -488,6 +514,13 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
} else { } else {
constants.add(sn.toString()); constants.add(sn.toString());
} }
} else if (retExpr 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());
}
} }
} }
} }
@@ -751,6 +784,25 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
return (MethodDeclaration) parent; return (MethodDeclaration) parent;
} }
private ASTNode findStatement(ASTNode node) {
while (node != null && !(node instanceof Statement)) {
node = node.getParent();
}
return node;
}
private String[] extractMethodSuffix(String paramName, String currentSuffix) {
int dotIndex = paramName.indexOf('.');
if (dotIndex > 0 && dotIndex + 1 < paramName.length() && Character.isLowerCase(paramName.charAt(dotIndex + 1))) {
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
}
return new String[] { paramName, currentSuffix };
}
private String parseEnumSetElement(String eVal) {
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
}
private Map<String, List<CallEdge>> buildCallGraph() { private Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>(); graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) { for (CompilationUnit cu : context.getCompilationUnits()) {
@@ -1168,35 +1220,18 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
} }
@Override @Override
public boolean visit(ConstructorInvocation node) { public boolean visit(ConstructorInvocation node) {
for (Object argObj : node.arguments()) { processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
Expression arg = (Expression) argObj;
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
return super.visit(node); return super.visit(node);
} }
@Override @Override
public boolean visit(SuperConstructorInvocation node) { public boolean visit(SuperConstructorInvocation node) {
for (Object argObj : node.arguments()) { TypeDeclaration enclosingTd = findEnclosingType(node);
Expression arg = (Expression) argObj; if (enclosingTd != null) {
String val = constantResolver.resolve(arg, context); String superFqn = context.getSuperclassFqn(enclosingTd);
if (val != null) { if (superFqn != null) {
if (val.startsWith("ENUM_SET:")) { TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
for (String eVal : val.substring(9).split(",")) { processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
} }
} }
return super.visit(node); return super.visit(node);
@@ -1220,4 +1255,123 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
return results; return results;
} }
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
if (targetTd == null) return;
MethodDeclaration callerMd = findEnclosingMethod(callNode);
org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null;
if (callNode instanceof ConstructorInvocation ci) {
resolvedBinding = ci.resolveConstructorBinding();
} else if (callNode instanceof SuperConstructorInvocation sci) {
resolvedBinding = sci.resolveConstructorBinding();
}
for (MethodDeclaration otherMd : targetTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>());
if (targetIdx >= 0 && targetIdx < arguments.size()) {
Expression arg = (Expression) arguments.get(targetIdx);
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(parseEnumSetElement(eVal));
}
} else {
results.add(val);
}
}
}
}
}
}
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set<MethodDeclaration> visited) {
if (constructorMd == null || constructorMd.getBody() == null) return -1;
if (!visited.add(constructorMd)) return -1;
final int[] foundIdx = {-1};
constructorMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment asn) {
Expression left = asn.getLeftHandSide();
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName))) {
Expression right = asn.getRightHandSide();
if (right instanceof SimpleName snRight) {
String rightName = snRight.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(rightName)) {
foundIdx[0] = i;
}
}
}
}
return super.visit(asn);
}
@Override
public boolean visit(ConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
for (MethodDeclaration otherMd : superTd.getMethods()) {
if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
}
}
return super.visit(node);
}
});
return foundIdx[0];
}
} }

View File

@@ -418,6 +418,25 @@ public class JdtCallGraphEngine implements CallGraphEngine {
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName); if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
String called = superFqn + "." + smi.getName().getIdentifier();
if (visited.contains(called)) {
handled = true;
} else {
String cName = superFqn;
String mName = smi.getName().getIdentifier();
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult); constants.addAll(delegationResult);
@@ -451,6 +470,13 @@ public class JdtCallGraphEngine implements CallGraphEngine {
} else { } else {
constants.add(sn.toString()); constants.add(sn.toString());
} }
} else if (retExpr 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());
}
} }
} }
} }
@@ -1135,35 +1161,18 @@ public class JdtCallGraphEngine implements CallGraphEngine {
} }
@Override @Override
public boolean visit(ConstructorInvocation node) { public boolean visit(ConstructorInvocation node) {
for (Object argObj : node.arguments()) { processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
Expression arg = (Expression) argObj;
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
return super.visit(node); return super.visit(node);
} }
@Override @Override
public boolean visit(SuperConstructorInvocation node) { public boolean visit(SuperConstructorInvocation node) {
for (Object argObj : node.arguments()) { TypeDeclaration enclosingTd = findEnclosingType(node);
Expression arg = (Expression) argObj; if (enclosingTd != null) {
String val = constantResolver.resolve(arg, context); String superFqn = context.getSuperclassFqn(enclosingTd);
if (val != null) { if (superFqn != null) {
if (val.startsWith("ENUM_SET:")) { TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
for (String eVal : val.substring(9).split(",")) { processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
} }
} }
return super.visit(node); return super.visit(node);
@@ -1187,4 +1196,123 @@ public class JdtCallGraphEngine implements CallGraphEngine {
return results; return results;
} }
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
if (targetTd == null) return;
MethodDeclaration callerMd = findEnclosingMethod(callNode);
org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null;
if (callNode instanceof ConstructorInvocation ci) {
resolvedBinding = ci.resolveConstructorBinding();
} else if (callNode instanceof SuperConstructorInvocation sci) {
resolvedBinding = sci.resolveConstructorBinding();
}
for (MethodDeclaration otherMd : targetTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>());
if (targetIdx >= 0 && targetIdx < arguments.size()) {
Expression arg = (Expression) arguments.get(targetIdx);
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
}
}
}
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set<MethodDeclaration> visited) {
if (constructorMd == null || constructorMd.getBody() == null) return -1;
if (!visited.add(constructorMd)) return -1;
final int[] foundIdx = {-1};
constructorMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment asn) {
Expression left = asn.getLeftHandSide();
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName))) {
Expression right = asn.getRightHandSide();
if (right instanceof SimpleName snRight) {
String rightName = snRight.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(rightName)) {
foundIdx[0] = i;
}
}
}
}
return super.visit(asn);
}
@Override
public boolean visit(ConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
for (MethodDeclaration otherMd : superTd.getMethods()) {
if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
}
}
return super.visit(node);
}
});
return foundIdx[0];
}
} }

View File

@@ -0,0 +1,275 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ConstructorInvocationTest {
@Test
void shouldNotBlindlyAddAllConstructorArguments(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
private String irrelevantInfo;
public RichOrderEvent() {
this(OrderEvents.PAY, "DEFAULT_INFO");
}
public RichOrderEvent(OrderEvents type, String info) {
this.type = type;
this.irrelevantInfo = info;
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
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())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO");
}
@Test
void shouldHandleOverloadedConstructorsGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
private String irrelevantInfo;
private int count;
public RichOrderEvent() {
this(OrderEvents.PAY, "DEFAULT_INFO");
}
// Overloaded constructor 1
public RichOrderEvent(OrderEvents type, String info) {
this.type = type;
this.irrelevantInfo = info;
}
// Overloaded constructor 2 (same number of arguments!)
public RichOrderEvent(String info, int count) {
this.irrelevantInfo = info;
this.count = count;
// type is not set here
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
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())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO")
.doesNotContain("count");
}
@Test
void shouldHandleChainedConstructorDelegationGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class BaseEvent {
protected OrderEvents type;
public BaseEvent(OrderEvents type) {
this.type = type;
}
}
class RichOrderEvent extends BaseEvent {
public RichOrderEvent() {
this(OrderEvents.PAY);
}
public RichOrderEvent(OrderEvents type) {
this(type, "DEFAULT_INFO");
}
public RichOrderEvent(OrderEvents type, String info) {
super(type);
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
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())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO");
}
@Test
void shouldHandleFieldAccessGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public RichOrderEvent() {
this(OrderEvents.PAY);
}
public RichOrderEvent(OrderEvents type) {
this.type = type;
}
public OrderEvents getType() {
return this.type; // Specifically testing FieldAccess
}
}
enum OrderEvents { PAY }
""";
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())
.contains("OrderEvents.PAY");
}
}

View File

@@ -79,6 +79,124 @@ class HeuristicCallGraphEngineTest {
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133"); .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
} }
@Test
void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventFromOldSwitch(vv));
}
private OrderEvents getEventFromOldSwitch(String q) {
switch (q) {
case "a": return OrderEvents.A8;
case "b":
case "c": return OrderEvents.A133;
default: return OrderEvents.PAY;
}
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventMultiValue(vv));
}
private OrderEvents getEventMultiValue(String q) {
return switch (q) {
case "a", "b" -> OrderEvents.A8;
case "c" -> OrderEvents.A133;
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventWithYield(vv));
}
private OrderEvents getEventWithYield(String q) {
return switch (q) {
case "a" -> {
System.out.println("Processing a");
yield OrderEvents.A8;
}
case "b" -> { yield OrderEvents.A133; }
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test @Test
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
String source = """ String source = """

View File

@@ -0,0 +1,71 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SuperMethodInvocationTest {
@Test
void shouldHandleSuperMethodInvocation(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(DerivedEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class BaseEvent {
public OrderEvents getType() {
return OrderEvents.PAY;
}
}
class DerivedEvent extends BaseEvent {
public OrderEvents getType() {
return super.getType();
}
}
enum OrderEvents { PAY }
""";
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())
.contains("OrderEvents.PAY");
}
}