diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java index fcfe4d3..92948b7 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java @@ -12,16 +12,24 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { return false; } - String triggerEvent = simplify(triggerPoint.getEvent()); + String rawTriggerEvent = triggerPoint.getEvent(); String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName(); String smEvent = simplify(smEventRaw); - boolean isWildcard = isWildcardVariable(triggerEvent); + boolean isWildcard = isWildcardVariable(rawTriggerEvent); List polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList(); boolean hasPolyMatch = false; for (String pe : polyEvents) { + if (pe.contains(".") && smEventRaw.contains(".")) { + if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe)) { + hasPolyMatch = true; + break; + } + continue; // Stricter matching: do not fallback if both have qualifiers but don't match + } + String simplePe = pe; if (pe.contains(".")) { simplePe = pe.substring(pe.lastIndexOf('.') + 1); @@ -35,7 +43,18 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { } } - return hasPolyMatch || smEvent.equals(triggerEvent) || (polyEvents.isEmpty() && isWildcard); + if (hasPolyMatch) return true; + if (polyEvents.isEmpty() && isWildcard) return true; + + if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) { + if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) { + return true; + } + return false; + } + + String triggerEvent = simplify(rawTriggerEvent); + return smEvent.equals(triggerEvent); } private boolean isWildcardVariable(String eventStr) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index f504f5b..07e40af 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -129,6 +129,29 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { String entryMethod = path.get(0); int entryParamIndex = getParameterIndex(entryMethod, currentParamName); if (entryParamIndex < 0) { + // Intercept local setter before falling back to initializer + if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) { + String localMethodName = methodSuffix.substring(1).replace("()", ""); + org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, currentParamName, localMethodName); + if (localSetterExpr != null) { + List setterEvents = new ArrayList<>(); + extractConstantsFromExpression(localSetterExpr, setterEvents); + if (!setterEvents.isEmpty()) { + 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(setterEvents) + .build(); + } + } + } + String tracedVar = traceLocalVariable(entryMethod, currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { int dotIdx = tracedVar.indexOf('.'); @@ -148,6 +171,7 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { exprParser.setSource(resolvedValue.toCharArray()); exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); + System.out.println("resolvedValue = " + resolvedValue + " exprNode=" + exprNode.getClass().getSimpleName()); String varName = null; String methodName = null; @@ -204,6 +228,29 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } if (methodName != null) { + System.out.println("Checking local setter for: varName=" + varName + " methodName=" + methodName + " entryMethod=" + entryMethod); + if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) { + org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, varName, methodName); + System.out.println("localSetterExpr = " + (localSetterExpr != null ? localSetterExpr.toString() : "null")); + if (localSetterExpr != null) { + extractConstantsFromExpression(localSetterExpr, polymorphicEvents); + System.out.println("Extracted from setter: " + polymorphicEvents); + if (!polymorphicEvents.isEmpty()) { + 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(); + } + } + } + if (varName != null && declaredType == null) { for (String methodFqn : path) { declaredType = getVariableDeclaredType(methodFqn, varName); @@ -423,22 +470,18 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } } if (!handled) { + extractConstantsFromExpression(retExpr, constants); String val = constantResolver.resolve(retExpr, context); if (val != null) { if (val.startsWith("ENUM_SET:")) { for (String eVal : val.substring(9).split(",")) { - constants.add(eVal.substring(eVal.lastIndexOf('.') + 1)); + String parsed = eVal.substring(eVal.lastIndexOf('.') + 1); + if (!constants.contains(parsed)) constants.add(parsed); } } else { - constants.add(val); + if (!constants.contains(val)) constants.add(val); } - } else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { - extractConstantsFromExpression(ce, constants); - } else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { - constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType())); - } else if (retExpr instanceof QualifiedName qn) { - constants.add(qn.toString()); - } else if (retExpr instanceof SimpleName sn) { + } else if (retExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { List consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); if (!consts.isEmpty()) { constants.addAll(consts); @@ -491,9 +534,87 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { extractConstantsFromExpression(pe.getExpression(), constants); } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { extractConstantsFromExpression(ce.getExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { + se.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.YieldStatement ys) { + extractConstantsFromExpression(ys.getExpression(), constants); + return super.visit(ys); + } + @Override + public boolean visit(org.eclipse.jdt.core.dom.ExpressionStatement es) { + extractConstantsFromExpression(es.getExpression(), constants); + return super.visit(es); + } + @Override + public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { + extractConstantsFromExpression(rs.getExpression(), constants); + return super.visit(rs); + } + }); + } else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + String methodName = mi.getName().getIdentifier(); + + // 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(); + String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName; + + org.eclipse.jdt.core.dom.Block block = findEnclosingBlock(mi); + if (block != null) { + for (Object stmtObj : block.statements()) { + if (stmtObj == mi.getParent() || stmtObj == mi) break; + if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { + if (es.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation setterMi) { + if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) { + if (setterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName setterSn && setterSn.getIdentifier().equals(varName)) { + if (!setterMi.arguments().isEmpty()) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) setterMi.arguments().get(0), constants); + return; + } + } + } + } else if (es.getExpression() instanceof org.eclipse.jdt.core.dom.Assignment assignment) { + if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { + if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) { + if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } else if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) { + if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { + if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } + } + } + } + } + } + + // 2. Delegate to method return analysis + TypeDeclaration td = findEnclosingType(mi); + System.out.println("DELEGATING MethodInvocation: " + methodName + " td=" + (td != null ? td.getName() : "null") + " fqn=" + (td != null ? context.getFqn(td) : "null")); + if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) { + List values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null); + System.out.println("RESOLVED MethodInvocation values: " + values); + if (values != null) constants.addAll(values); + } } } + private 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)) { + parent = parent.getParent(); + } + return (org.eclipse.jdt.core.dom.Block) parent; + } + private String traceLocalVariable(String methodFqn, String varName) { if (methodFqn == null || !methodFqn.contains(".")) return null; String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); @@ -546,6 +667,52 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { return null; } + private org.eclipse.jdt.core.dom.Expression traceLocalSetter(String methodFqn, String varName, String getterName) { + if (methodFqn == null || !methodFqn.contains(".")) return null; + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + final org.eclipse.jdt.core.dom.Expression[] setterArg = new org.eclipse.jdt.core.dom.Expression[1]; + String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName; + md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) { + if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) { + if (node.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn && sn.getIdentifier().equals(varName)) { + if (!node.arguments().isEmpty()) { + setterArg[0] = (org.eclipse.jdt.core.dom.Expression) node.arguments().get(0); + } + } + } + return super.visit(node); + } + @Override + public boolean visit(org.eclipse.jdt.core.dom.Assignment node) { + if (node.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { + if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) { + if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { + setterArg[0] = node.getRightHandSide(); + } + } + } else if (node.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) { + if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { + if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { + setterArg[0] = node.getRightHandSide(); + } + } + } + return super.visit(node); + } + }); + return setterArg[0]; + } + } + return null; + } + private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { if (depth > 5) return mi; if (!mi.arguments().isEmpty()) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 117d7c0..673da84 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -427,14 +427,16 @@ public class JdtCallGraphEngine implements CallGraphEngine { } } if (!handled) { + extractConstantsFromExpression(retExpr, constants); String val = constantResolver.resolve(retExpr, context); if (val != null) { if (val.startsWith("ENUM_SET:")) { for (String eVal : val.substring(9).split(",")) { - constants.add(eVal.substring(eVal.lastIndexOf('.') + 1)); + String parsed = eVal.substring(eVal.lastIndexOf('.') + 1); + if (!constants.contains(parsed)) constants.add(parsed); } } else { - constants.add(val); + if (!constants.contains(val)) constants.add(val); } } else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { extractConstantsFromExpression(ce, constants); @@ -494,9 +496,85 @@ public class JdtCallGraphEngine implements CallGraphEngine { extractConstantsFromExpression(pe.getExpression(), constants); } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { extractConstantsFromExpression(ce.getExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { + se.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.YieldStatement ys) { + extractConstantsFromExpression(ys.getExpression(), constants); + return super.visit(ys); + } + @Override + public boolean visit(org.eclipse.jdt.core.dom.ExpressionStatement es) { + extractConstantsFromExpression(es.getExpression(), constants); + return super.visit(es); + } + @Override + public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { + extractConstantsFromExpression(rs.getExpression(), constants); + return super.visit(rs); + } + }); + } else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + String methodName = mi.getName().getIdentifier(); + + // 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(); + String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName; + + org.eclipse.jdt.core.dom.Block block = findEnclosingBlock(mi); + if (block != null) { + for (Object stmtObj : block.statements()) { + if (stmtObj == mi.getParent() || stmtObj == mi) break; + if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { + if (es.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation setterMi) { + if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) { + if (setterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName setterSn && setterSn.getIdentifier().equals(varName)) { + if (!setterMi.arguments().isEmpty()) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) setterMi.arguments().get(0), constants); + return; + } + } + } + } else if (es.getExpression() instanceof org.eclipse.jdt.core.dom.Assignment assignment) { + if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { + if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) { + if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } else if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) { + if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { + if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } + } + } + } + } + } + + // 2. 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 values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null); + if (values != null) constants.addAll(values); + } } } + private 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)) { + parent = parent.getParent(); + } + return (org.eclipse.jdt.core.dom.Block) parent; + } + private String traceLocalVariable(String methodFqn, String varName) { if (methodFqn == null || !methodFqn.contains(".")) return null; String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java new file mode 100644 index 0000000..032e0fa --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java @@ -0,0 +1,78 @@ +package click.kamil.springstatemachineexporter.analysis.enricher.matching; + +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.model.Event; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeuristicEventMatchingEngineTest { + + private final HeuristicEventMatchingEngine engine = new HeuristicEventMatchingEngine(); + + @Test + void shouldMatchExactFqn() { + Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.OrderEvents.PAY").build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } + + @Test + void shouldMatchEnumQualifierToFqn() { + Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } + + @Test + void shouldPreventCrossEnumContamination() { + Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); + } + + @Test + void shouldPreventCrossEnumContaminationWithPolymorphicEvents() { + Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); + } + + @Test + void shouldMatchPolymorphicEnumQualifierToFqn() { + Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } + + @Test + void shouldMatchRawStringToFqn() { + Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } + + @Test + void shouldMatchRawStringToRawString() { + Event smEvent = Event.of("PAY", "PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } + + @Test + void shouldMatchPolymorphicRawStringToFqn() { + Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("PAY")).build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index 0909b95..1c421a9 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -18,6 +18,67 @@ import static org.assertj.core.api.Assertions.assertThat; @Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis") class HeuristicCallGraphEngineTest { + @Test + void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderService { + public void updateOrderState(String vv) { + StateMachine sm = new StateMachine(); + MysteryPayload a = new MysteryPayload(); + a.setType(my_func(vv)); + sm.sendEvent(a.getType()); + } + + private OrderEvents my_func(String q) { + return switch (q) { + case "a" -> OrderEvents.A8; + case "b" -> OrderEvents.A133; + default -> throw new IllegalArgumentException("Unknown"); + }; + } + } + + enum OrderEvents { A8, A133 } + + class MysteryPayload { + private OrderEvents type; + public void setType(OrderEvents type) { this.type = type; } + public OrderEvents getType() { return type; } + } + + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + + System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents()); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133"); + } + @Test void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { String source = """