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 a46dc9d..50813a5 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 @@ -29,6 +29,13 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { } String classPe = pe.substring(0, pe.lastIndexOf('.')); String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.')); + if (classPe.contains(".") && classSm.contains(".")) { + String p1 = classPe.substring(0, classPe.lastIndexOf('.')); + String p2 = classSm.substring(0, classSm.lastIndexOf('.')); + if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) { + continue; + } + } String simpleClassPe = classPe.contains(".") ? classPe.substring(classPe.lastIndexOf('.') + 1) : classPe; String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm; if (!simpleClassPe.equals(simpleClassSm)) { @@ -59,6 +66,13 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { } String classTrig = rawTriggerEvent.substring(0, rawTriggerEvent.lastIndexOf('.')); String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.')); + if (classTrig.contains(".") && classSm.contains(".")) { + String p1 = classTrig.substring(0, classTrig.lastIndexOf('.')); + String p2 = classSm.substring(0, classSm.lastIndexOf('.')); + if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) { + return false; + } + } String simpleClassTrig = classTrig.contains(".") ? classTrig.substring(classTrig.lastIndexOf('.') + 1) : classTrig; String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm; if (!simpleClassTrig.equals(simpleClassSm)) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java index 4e8b1b3..1e8c860 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java @@ -20,7 +20,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { String machineEventFqn = machineTypes[1]; if (triggerEventFqn != null && machineEventFqn != null) { - if (triggerEventFqn.equals(machineEventFqn)) { + if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) { return true; // Match! } if (isTypeMismatched(triggerEventFqn, machineEventFqn)) { @@ -28,7 +28,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { } } if (triggerStateFqn != null && machineStateFqn != null) { - if (triggerStateFqn.equals(machineStateFqn)) { + if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) { return true; // Match! } if (isTypeMismatched(triggerStateFqn, machineStateFqn)) { @@ -314,6 +314,15 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { return new String[]{null, null}; } + private String eraseGenerics(String type) { + if (type == null) return null; + int idx = type.indexOf('<'); + if (idx != -1) { + return type.substring(0, idx); + } + return type; + } + private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) { if (type == null) return null; String simpleName; @@ -325,6 +334,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { simpleName = type.toString(); } + if (simpleName.contains("<")) { + simpleName = simpleName.substring(0, simpleName.indexOf('<')); + } + org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu); if (td != null) { return context.getFqn(td); @@ -349,6 +362,8 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { private boolean isTypeMismatched(String type1, String type2) { if (type1 == null || type2 == null) return false; + type1 = eraseGenerics(type1); + type2 = eraseGenerics(type2); if (type1.equals("java.lang.String") || type1.equals("String") || type1.equals("java.lang.Object") || type1.equals("Object")) return false; if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false; return !type1.equals(type2); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index d6ab77a..5d758a5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -170,6 +170,9 @@ public class ConstantResolver { String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier(); if (!visited.add(methodFqn)) { + if (log.isDebugEnabled()) { + log.debug("Cycle detected in method evaluation for: {} (visited={})", methodFqn, visited); + } return null; // Cycle detected in method evaluation } @@ -213,6 +216,9 @@ public class ConstantResolver { String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier(); if (!visited.add(methodFqn)) { + if (log.isDebugEnabled()) { + log.debug("Cycle detected in method body evaluation for: {} (visited={})", methodFqn, visited); + } return null; // Cycle detected in method evaluation } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index fea6a3d..305d382 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -959,7 +959,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (depth > 5) return mi; 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")) { + if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) { return mi; } } else { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java index 874abf3..0633b04 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java @@ -15,7 +15,12 @@ public class CallGraphPathFinder { public List findPath(String start, String target, Map> graph, Set visited) { if (start.equals(target)) return new ArrayList<>(List.of(start)); - if (!visited.add(start)) return null; // Path-scoped cycle detection + if (!visited.add(start)) { + if (log.isDebugEnabled()) { + log.debug("Cycle detected in call graph search: {} is already in visited path {}", start, visited); + } + return null; // Path-scoped cycle detection + } List neighbors = graph.get(start); if (neighbors != null) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java index 3986962..cde5ea5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java @@ -87,7 +87,8 @@ public class ConstantExtractor { } return; } - if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry")) { + if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") || + methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) { for (Object argObj : mi.arguments()) { extractConstantsFromExpression((Expression) argObj, constants); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java index c4bcf9e..4ff8c3f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -36,7 +36,17 @@ public class GenericEventDetector { if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) || "sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) { processSendEvent(node, cu, triggers); - } + } else { + for (Object argObj : node.arguments()) { + if (argObj instanceof ExpressionMethodReference emr) { + String refMethod = emr.getName().getIdentifier(); + if ("sendEvent".equals(refMethod) || "sendEvents".equals(refMethod) || + "sendEventCollect".equals(refMethod) || "sendEventMono".equals(refMethod)) { + processSendEventMethodReference(node, emr, cu, triggers); + } + } + } + } processHints(node, cu, triggers); @@ -46,6 +56,81 @@ public class GenericEventDetector { return triggers; } + private void processSendEventMethodReference(MethodInvocation node, ExpressionMethodReference emr, CompilationUnit cu, List triggers) { + Expression receiver = node.getExpression(); + String eventValue = extractEventFromReceiver(receiver); + if (eventValue == null) { + eventValue = receiver != null ? receiver.toString() : null; + } + + MethodDeclaration method = findEnclosingMethod(node); + TypeDeclaration type = findEnclosingType(node); + if (type == null) return; + + String sourceState = extractSourceState(node); + String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node); + + if (eventValue != null) { + triggers.add(TriggerPoint.builder() + .event(eventValue) + .sourceState(sourceState) + .className(context.getFqn(type)) + .methodName(method != null ? method.getName().getIdentifier() : "initializer") + .sourceFile(context.getRelativePath(context.getFqn(type))) + .lineNumber(cu.getLineNumber(node.getStartPosition())) + .stateTypeFqn(smTypes[0]) + .eventTypeFqn(smTypes[1]) + .build()); + } + } + + private String extractEventFromReceiver(Expression receiver) { + if (receiver == null) return null; + if (receiver instanceof MethodInvocation mi) { + String mName = mi.getName().getIdentifier(); + if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) { + Expression arg = (Expression) mi.arguments().get(0); + String resolved = constantResolver.resolve(arg, context); + return resolved != null ? resolved : arg.toString(); + } + if (mi.getExpression() != null) { + return extractEventFromReceiver(mi.getExpression()); + } + } + if (receiver instanceof SimpleName sn) { + return sn.getIdentifier(); + } + return null; + } + + private String[] resolveStateMachineTypeArgumentsForExpression(Expression receiver, MethodInvocation node) { + if (receiver == null) return new String[]{null, null}; + + ITypeBinding binding = receiver.resolveTypeBinding(); + if (binding != null) { + ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>()); + if (smBinding != null) { + ITypeBinding[] typeArgs = smBinding.getTypeArguments(); + if (typeArgs.length >= 2) { + return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()}; + } + } + } + + Type declType = findReceiverTypeManually(receiver, node); + if (declType instanceof ParameterizedType pt) { + List typeArgs = pt.typeArguments(); + if (typeArgs.size() >= 2) { + CompilationUnit compilationUnit = (CompilationUnit) node.getRoot(); + String stateType = resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit); + String eventType = resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit); + return new String[]{stateType, eventType}; + } + } + + return new String[]{null, null}; + } + private void processSendEvent(MethodInvocation node, CompilationUnit cu, List triggers) { List builtTriggers = buildTriggerPoints(node, cu, null); if (builtTriggers != null) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java index 9b3b697..aaf18a1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java @@ -546,7 +546,7 @@ public class VariableTracer { if (depth > 5) return mi; 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")) { + if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) { return mi; } } else { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java index e18b409..46295d5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java @@ -361,6 +361,115 @@ public class AstTransitionParser { return mr.toString(); } + // 5. Bean Reference (SimpleName, FieldAccess, etc.) + String resolvedBeanBody = resolveBeanClassBody(expr, context); + if (resolvedBeanBody != null) { + return resolvedBeanBody; + } + + return null; + } + + private static String resolveBeanClassBody(Expression expr, CodebaseContext context) { + String typeFqn = null; + ITypeBinding binding = expr.resolveTypeBinding(); + if (binding != null) { + typeFqn = binding.getQualifiedName(); + } else if (expr instanceof SimpleName sn) { + // Check enclosing method first for local variables + MethodDeclaration enclosingMethod = findEnclosingMethod(expr); + if (enclosingMethod != null) { + final String[] foundType = {null}; + final Expression[] foundInitializer = {null}; + enclosingMethod.accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationStatement vds) { + for (Object fragObj : vds.fragments()) { + if (fragObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) { + foundType[0] = vds.getType().toString(); + foundInitializer[0] = fragment.getInitializer(); + } + } + } + return super.visit(vds); + } + }); + if (foundInitializer[0] != null) { + ASTNode root = expr.getRoot(); + if (root instanceof CompilationUnit cu) { + String initLogic = extractInternalLogic(foundInitializer[0], cu, context); + if (initLogic != null) return initLogic; + } + } + if (foundType[0] != null) { + typeFqn = foundType[0]; + } + } + + if (typeFqn == null) { + TypeDeclaration enclosingClass = findEnclosingType(expr); + if (enclosingClass != null) { + for (FieldDeclaration fd : enclosingClass.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) { + if (fragment.getInitializer() != null) { + ASTNode root = expr.getRoot(); + if (root instanceof CompilationUnit cu) { + String initLogic = extractInternalLogic(fragment.getInitializer(), cu, context); + if (initLogic != null) return initLogic; + } + } + typeFqn = fd.getType().toString(); + break; + } + } + } + } + } + } + } + + if (typeFqn != null) { + if (typeFqn.contains("<")) { + typeFqn = typeFqn.substring(0, typeFqn.indexOf('<')); + } + TypeDeclaration beanTd = context.getTypeDeclaration(typeFqn); + if (beanTd == null) { + ASTNode root = expr.getRoot(); + if (root instanceof CompilationUnit cu) { + for (Object impObj : cu.imports()) { + ImportDeclaration imp = (ImportDeclaration) impObj; + if (!imp.isStatic() && !imp.isOnDemand()) { + String impName = imp.getName().getFullyQualifiedName(); + if (impName.endsWith("." + typeFqn)) { + beanTd = context.getTypeDeclaration(impName); + break; + } + } + } + } + } + if (beanTd != null) { + for (MethodDeclaration md : beanTd.getMethods()) { + String name = md.getName().getIdentifier(); + if ("evaluate".equals(name) || "execute".equals(name)) { + return md.toString(); + } + } + return beanTd.toString(); + } + } + return null; + } + + private static MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode current = node; + while (current != null) { + if (current instanceof MethodDeclaration md) return md; + current = current.getParent(); + } return null; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index dcbdd2c..04ace62 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -356,11 +356,20 @@ public class CodebaseContext { public List getEnumValues(String fqnOrSimpleName) { if (fqnOrSimpleName == null) return null; - List values = enumValues.get(fqnOrSimpleName); + + String cleanName = fqnOrSimpleName; + if (cleanName.contains("<")) { + cleanName = cleanName.substring(0, cleanName.indexOf('<')); + } + if (cleanName.contains("[")) { + cleanName = cleanName.substring(0, cleanName.indexOf('[')); + } + cleanName = cleanName.trim(); + + List values = enumValues.get(cleanName); if (values != null) return values; - String fqn = simpleNameToFqn.get(fqnOrSimpleName); + String fqn = simpleNameToFqn.get(cleanName); if (fqn != null) return enumValues.get(fqn); - // Also try stripping array or generic types if any (e.g. MyEnum[]) return null; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java index 9793225..558f8ce 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java @@ -136,6 +136,12 @@ public class JdtDataFlowModel implements DataFlowModel { // 4. Handle Method Invocations (Static Factory methods / Transforming Getters) if (expr instanceof MethodInvocation mi) { + String mName = mi.getName().getIdentifier(); + if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) { + List resolved = getReachingDefinitions((Expression) mi.arguments().get(0), visited, paramBindings, instanceFieldBindings, depth + 1); + visited.remove(expr); + return resolved; + } IMethodBinding mb = mi.resolveMethodBinding(); if (mb != null) { List targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth); 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 index b41d56b..ee7a2dd 100644 --- 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 @@ -112,7 +112,7 @@ class HeuristicEventMatchingEngineTest { Event smEvent = Event.of("CREATE", "com.example.other.OrderEvent.CREATE"); TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.some.OrderEvent.CREATE").build(); - assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); } @Test @@ -123,6 +123,6 @@ class HeuristicEventMatchingEngineTest { .polymorphicEvents(List.of("com.example.some.OrderEvent.CREATE")) .build(); - assertThat(engine.matches(smEvent, triggerPoint)).isTrue(); + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java index 3b9016d..c174242 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java @@ -1003,4 +1003,37 @@ class HeuristicCallGraphEngineCoreTest { 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 shouldResolveEventFromReactiveMethodReference(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private StateMachine stateMachine; + public void handleReactive() { + Mono.just("REACTIVE_EVENT").flatMap(stateMachine::sendEvent); + } + } + + class StateMachine { + public void sendEvent(String event) {} + } + + class Mono { + public static Mono just(Object obj) { return new Mono(); } + public Mono flatMap(Object mapper) { return this; } + } + """; + 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("handleReactive").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build(); + List 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("REACTIVE_EVENT"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParserTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParserTest.java index b306380..e17756b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParserTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParserTest.java @@ -326,4 +326,71 @@ class AstTransitionParserTest { } return null; } + + @Test + void shouldExtractBeanClassBodyForExternalGuard(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tempDir) throws java.io.IOException { + String guardSource = """ + package com.example; + import org.springframework.statemachine.guard.Guard; + import org.springframework.statemachine.StateContext; + public class CustomGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + return "OK".equals(context.getMessage().getPayload()); + } + } + """; + String configSource = """ + package com.example; + import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + public class TestClass { + private CustomGuard paymentGuard; + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions.withExternal() + .source("CREATED") + .target("PAID") + .event("PAY") + .guard(paymentGuard); + } + } + """; + java.nio.file.Files.writeString(tempDir.resolve("CustomGuard.java"), guardSource); + java.nio.file.Files.writeString(tempDir.resolve("TestClass.java"), configSource); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.example.TestClass"); + MethodDeclaration method = context.findMethodDeclaration(td, "configure", true); + + List transitions = AstTransitionParser.parseTransitions(method, context); + + assertThat(transitions).hasSize(1); + assertThat(transitions.getFirst().getGuard().internalLogic()) + .contains("return \"OK\".equals(context.getMessage().getPayload());") + .contains("public boolean evaluate"); + } + + @Test + void shouldExtractLocalVariableLambdaGuard() { + String source = """ + import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + import org.springframework.statemachine.guard.Guard; + public class TestClass { + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + Guard myLocalGuard = context -> "LOCAL_OK".equals(context.getMessage()); + transitions.withExternal() + .source("CREATED") + .target("PAID") + .event("PAY") + .guard(myLocalGuard); + } + } + """; + MethodDeclaration method = createMethodDeclaration(source); + + List transitions = AstTransitionParser.parseTransitions(method, context); + + assertThat(transitions).hasSize(1); + assertThat(transitions.getFirst().getGuard().internalLogic()) + .contains("context -> \"LOCAL_OK\".equals(context.getMessage())"); + } }