diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index b39baf4..f9f891b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -57,7 +57,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { // rather than completely hiding the endpoint. } - if (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())) { + List polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList(); + boolean hasPolyMatch = false; + for (String pe : polyEvents) { + String simplePe = pe; + if (pe.contains(".")) { + simplePe = pe.substring(pe.lastIndexOf('.') + 1); + } + if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) { + hasPolyMatch = true; + break; + } + } + + if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) { // Event matches or is a wildcard for (State smSourceState : t.getSourceStates()) { String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java index ba0263e..ea2ae00 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -20,4 +20,5 @@ public class TriggerPoint { private final String stateMachineId; // Optional: to link to a specific SM instance private final String sourceState; // Optional: if we can determine the expected current state private final int lineNumber; + private final java.util.List polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism } 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 04dba49..25f76e1 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 @@ -48,7 +48,8 @@ public class ConstantResolver { return resolveInfix(infix, context, visited); } if (expr instanceof QualifiedName qn) { - return resolveManual(qn, context, visited); + String val = resolveManual(qn, context, visited); + return val != null ? val : qn.toString(); } if (expr instanceof SimpleName sn) { return resolveManual(sn, context, visited); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java index 3fb054b..cebfef2 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -88,8 +88,14 @@ public class CallGraphBuilder { // Not a parameter. Maybe it's a local variable initialized from a parameter or method call? String tracedVar = traceLocalVariable(target, currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { + // Extract method calls like .getType() from the traced variable + int dotIdx = tracedVar.indexOf('.'); + if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) { + methodSuffix = tracedVar.substring(dotIdx) + methodSuffix; + tracedVar = tracedVar.substring(0, dotIdx); + } currentParamName = tracedVar; - // We must try to find paramIndex again, because the traced variable might be a parameter of this same method + resolvedValue = tracedVar + methodSuffix; paramIndex = getParameterIndex(target, currentParamName); } } @@ -106,6 +112,12 @@ public class CallGraphBuilder { if (paramIndex < edge.getArguments().size()) { String arg = edge.getArguments().get(paramIndex); if (arg != null) { + // If the argument passed has a method call, extract it + int dotIdx = arg.indexOf('.'); + if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) { + methodSuffix = arg.substring(dotIdx) + methodSuffix; + arg = arg.substring(0, dotIdx); + } currentParamName = arg; resolvedValue = arg + methodSuffix; found = true; @@ -118,13 +130,73 @@ public class CallGraphBuilder { if (!found) break; // Could not map argument } - if (!resolvedValue.equals(event)) { + // Final check on the entry method + String entryMethod = path.get(0); + int entryParamIndex = getParameterIndex(entryMethod, currentParamName); + if (entryParamIndex < 0) { + String tracedVar = traceLocalVariable(entryMethod, currentParamName); + if (tracedVar != null && !tracedVar.equals(currentParamName)) { + int dotIdx = tracedVar.indexOf('.'); + if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) { + methodSuffix = tracedVar.substring(dotIdx) + methodSuffix; + tracedVar = tracedVar.substring(0, dotIdx); + } + currentParamName = tracedVar; + resolvedValue = tracedVar + methodSuffix; + } + } + + List polymorphicEvents = new ArrayList<>(); + if (resolvedValue.matches(".*\\.get[A-Z].*\\(\\)")) { + String varName = resolvedValue.substring(0, resolvedValue.indexOf('.')); + String methodName = resolvedValue.substring(resolvedValue.indexOf('.') + 1, resolvedValue.indexOf('(')); + + // Resolve in the first method in the path where the variable might be declared + for (String methodFqn : path) { + String declaredType = getVariableDeclaredType(methodFqn, varName); + if (declaredType != null) { + System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType); + List typesToInspect = new ArrayList<>(); + typesToInspect.add(declaredType); + typesToInspect.addAll(context.getImplementations(declaredType)); + System.out.println("DEEP TRACE IMPLS: " + typesToInspect); + + for (String type : typesToInspect) { + Set visited = new HashSet<>(); + // We must find the compilation unit to pass for simple names! + // Let's pass the first available CU for this class + TypeDeclaration baseTd = context.getTypeDeclaration(type); + CompilationUnit cuToUse = null; + if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) { + cuToUse = (CompilationUnit) baseTd.getRoot(); + } else { + String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName); + if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) { + cuToUse = (CompilationUnit) entryTd.getRoot(); + } + } + List constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse); + System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants); + for (String constant : constants) { + if (!polymorphicEvents.contains(constant)) { + polymorphicEvents.add(constant); + } + } + } + break; + } + } + } + + if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() .event(resolvedValue) .className(tp.getClassName()) .methodName(tp.getMethodName()) .sourceFile(tp.getSourceFile()) .lineNumber(tp.getLineNumber()) + .polymorphicEvents(polymorphicEvents) .build(); } @@ -150,6 +222,108 @@ public class CallGraphBuilder { return -1; } + private 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); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null) { + for (Object pObj : md.parameters()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; + if (svd.getName().getIdentifier().equals(varName)) { + return svd.getType().toString(); + } + } + final String[] foundType = new String[1]; + if (md.getBody() != null) { + 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)) { + foundType[0] = node.getType().toString(); + } + } + return super.visit(node); + } + }); + } + return foundType[0]; + } + } + return null; + } + + private List resolveMethodReturnConstant(String className, String methodName, int depth, Set visited, CompilationUnit contextCu) { + if (depth > 20) return Collections.emptyList(); + String fqn = className + "." + methodName; + if (!visited.add(fqn)) return Collections.emptyList(); + + List constants = new ArrayList<>(); + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); + if (td == null) { + System.out.println("DEEP TRACE FAILED TO FIND TD: " + className); + } + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + Expression retExpr = node.getExpression(); + if (retExpr != null) { + boolean handled = false; + if (retExpr instanceof MethodInvocation mi) { + // Follow delegation first + String called = resolveCalledMethod(mi); + System.out.println("DEEP TRACE RESOLVED CALLED: " + called); + if (called != null && called.contains(".")) { + if (visited.contains(called)) { + handled = true; + } else { + String cName = called.substring(0, called.lastIndexOf('.')); + String mName = called.substring(called.lastIndexOf('.') + 1); + + 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) { + List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + handled = true; + } + } + } + } + if (!handled) { + 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)); + } + } else { + constants.add(val); + } + } else if (retExpr instanceof QualifiedName qn) { + constants.add(qn.toString()); + } else if (retExpr instanceof SimpleName sn) { + constants.add(sn.toString()); + } + } + } + return super.visit(node); + } + }); + } + } + visited.remove(fqn); + return constants; + } + private String traceLocalVariable(String methodFqn, String varName) { if (methodFqn == null || !methodFqn.contains(".")) return null; String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); @@ -179,11 +353,18 @@ public class CallGraphBuilder { if (initializer[0] != null) { Expression expr = traceVariable(initializer[0]); if (expr instanceof MethodInvocation mi) { - // e.g. event.getType() -> return "event.getType()" or just "event" - if (mi.getExpression() instanceof SimpleName sn) { - return sn.getIdentifier() + "." + mi.getName().getIdentifier() + "()"; + // Unwrapper logic: If wrapper method is called, extract its arguments recursively + Expression innerMost = unwrapMethodInvocation(mi, 0); + if (innerMost instanceof MethodInvocation innerMi) { + if (innerMi.getExpression() instanceof SimpleName sn) { + return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()"; + } + return innerMi.getName().getIdentifier() + "()"; } - return mi.getName().getIdentifier() + "()"; + if (innerMost instanceof SimpleName sn) { + return sn.getIdentifier(); + } + return innerMost.toString(); } if (expr instanceof SimpleName sn) { return sn.getIdentifier(); @@ -195,6 +376,18 @@ public class CallGraphBuilder { return null; } + private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { + if (depth > 5) return mi; + if (!mi.arguments().isEmpty()) { + Expression arg = (Expression) mi.arguments().get(0); + if (arg instanceof MethodInvocation innerMi) { + return unwrapMethodInvocation(innerMi, depth + 1); + } + return arg; + } + return mi; + } + private String extractContextMachineId(List path, Map> callGraph) { for (String node : path) { List edges = callGraph.get(node); @@ -213,53 +406,58 @@ public class CallGraphBuilder { return null; } + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } + private Map> buildCallGraph() { graph = new HashMap<>(); for (CompilationUnit cu : context.getCompilationUnits()) { cu.accept(new ASTVisitor() { - @Override - public boolean visit(MethodDeclaration node) { - TypeDeclaration td = findEnclosingType(node); - if (td != null) { - currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier(); - } - return super.visit(node); - } @Override public boolean visit(MethodInvocation node) { - if (currentMethodFqn != null) { - List calledMethods = resolveCalledMethodsPolymorphic(node); - List args = resolveArguments(node.arguments()); - for (String calledMethod : calledMethods) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); - } - for (Object argObj : node.arguments()) { - if (argObj instanceof ExpressionMethodReference emr) { - String typeName = emr.getExpression().toString(); - if ("this".equals(typeName) || "super".equals(typeName)) { - TypeDeclaration td = findEnclosingType(node); - if (td != null) { - String refMethod = resolveMethodInType(td, emr.getName().getIdentifier()); - if (refMethod != null) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); + MethodDeclaration md = findEnclosingMethod(node); + if (md != null) { + TypeDeclaration td = findEnclosingType(md); + if (td != null) { + String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier(); + List calledMethods = resolveCalledMethodsPolymorphic(node); + List args = resolveArguments(node.arguments()); + for (String calledMethod : calledMethods) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); + } + for (Object argObj : node.arguments()) { + if (argObj instanceof ExpressionMethodReference emr) { + String typeName = emr.getExpression().toString(); + if ("this".equals(typeName) || "super".equals(typeName)) { + TypeDeclaration td2 = findEnclosingType(node); + if (td2 != null) { + String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); + if (refMethod != null) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); + } } - } - } else { - String fallbackTypeFqn = null; - ITypeBinding binding = emr.getExpression().resolveTypeBinding(); - if (binding != null) { - fallbackTypeFqn = binding.getQualifiedName(); - } else if (emr.getExpression() instanceof SimpleName sn) { - fallbackTypeFqn = resolveReceiverTypeFallback(sn); - } - if (fallbackTypeFqn != null) { - String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); - - List impls = context.getImplementations(fallbackTypeFqn); - for (String impl : impls) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); + } else { + String fallbackTypeFqn = null; + ITypeBinding binding = emr.getExpression().resolveTypeBinding(); + if (binding != null) { + fallbackTypeFqn = binding.getQualifiedName(); + } else if (emr.getExpression() instanceof SimpleName sn) { + fallbackTypeFqn = resolveReceiverTypeFallback(sn); + } + if (fallbackTypeFqn != null) { + String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); + + List impls = context.getImplementations(fallbackTypeFqn); + for (String impl : impls) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); + } } } } @@ -271,22 +469,27 @@ public class CallGraphBuilder { @Override public boolean visit(SuperMethodInvocation node) { - if (currentMethodFqn != null) { - String methodName = node.getName().getIdentifier(); - TypeDeclaration td = findEnclosingType(node); - if (td != null) { - String superFqn = context.getSuperclassFqn(td); - if (superFqn != null) { - TypeDeclaration superTd = context.getTypeDeclaration(superFqn); - String calledMethod = null; - if (superTd != null) { - calledMethod = resolveMethodInType(superTd, methodName); + MethodDeclaration md = findEnclosingMethod(node); + if (md != null) { + TypeDeclaration tdOuter = findEnclosingType(md); + if (tdOuter != null) { + String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier(); + String methodName = node.getName().getIdentifier(); + TypeDeclaration td = findEnclosingType(node); + if (td != null) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + String calledMethod = null; + if (superTd != null) { + calledMethod = resolveMethodInType(superTd, methodName); + } + if (calledMethod == null) { + calledMethod = superFqn + "." + methodName; + } + List args = resolveArguments(node.arguments()); + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); } - if (calledMethod == null) { - calledMethod = superFqn + "." + methodName; - } - List args = resolveArguments(node.arguments()); - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); } } } @@ -333,7 +536,11 @@ public class CallGraphBuilder { } // Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...)) - expr = traceVariable(expr); + 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 + } + if (expr instanceof ClassInstanceCreation cic) { if (!cic.arguments().isEmpty()) { expr = (Expression) cic.arguments().get(0); @@ -386,6 +593,13 @@ public class CallGraphBuilder { return binding.getQualifiedName() + "." + methodName; } + if (receiver instanceof ThisExpression) { + TypeDeclaration td = findEnclosingType(node); + if (td != null) { + return context.getFqn(td) + "." + methodName; + } + } + if (receiver instanceof SimpleName sn) { String fallbackTypeFqn = resolveReceiverTypeFallback(sn); if (fallbackTypeFqn != null) { @@ -558,12 +772,4 @@ public class CallGraphBuilder { } return expr; } - - private MethodDeclaration findEnclosingMethod(ASTNode node) { - ASTNode parent = node.getParent(); - while (parent != null && !(parent instanceof MethodDeclaration)) { - parent = parent.getParent(); - } - return (MethodDeclaration) parent; - } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java index 4ba164b..9db6d32 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java @@ -215,8 +215,743 @@ class CallGraphBuilderTest { List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) + .containsExactlyInAnyOrder("OrderEvents.PAY"); + } + + @Test + void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + doProcessOrderEvent(domainEvent); + } + + private void doProcessOrderEvent(RichOrderEvent domainEvent) { + OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType()); + service.updateOrderState(eventType); + } + + private OrderEvents assertSupportedOrderEvent(OrderEvents e) { + return e; + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) { + // sendEvent + } + } + + interface RichOrderEvent { + OrderEvents getType(); + } + + class CancelOrderEvent implements RichOrderEvent { + public OrderEvents getType() { return OrderEvents.CANCELLED; } + } + + class ReceiveOrderEvent implements RichOrderEvent { + public OrderEvents getType() { return OrderEvents.RECEIVED; } + } + + enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_poly"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) + .containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED"); + } + + @Test + void shouldUnwrapDeepMethodWrappers() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType()))); + service.updateOrderState(eventType); + } + + private OrderEvents wrap1(OrderEvents e) { return e; } + private OrderEvents wrap2(OrderEvents e) { return e; } + private OrderEvents wrap3(OrderEvents e) { return e; } + } + + class OrderService { + public void updateOrderState(OrderEvents event) { } + } + + class RichOrderEvent { + public OrderEvents getType() { return OrderEvents.CREATE; } + } + + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) + .containsExactlyInAnyOrder("OrderEvents.CREATE"); + } + + @Test + void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(BaseEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + interface BaseEvent { + OrderEvents getType(); + } + + abstract class AbstractEvent implements BaseEvent { + protected OrderEvents internalGetType() { + return OrderEvents.PAY; + } + } + + class ConcreteEvent extends AbstractEvent { + public OrderEvents getType() { + return internalGetType(); + } + } + + class AnotherConcreteEvent implements BaseEvent { + public OrderEvents getType() { + return delegateToHelper(); + } + private OrderEvents delegateToHelper() { + return OrderEvents.CANCELLED; + } + } + + enum OrderEvents { PAY, CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED"); + } + + @Test + void shouldHandleMissingImplementationsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(UnknownEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + // UnknownEvent is not defined in the source (e.g. from a third-party library) + + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_missing_impl"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); CallChain chain = chains.get(0); assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty(); + } + + @Test + void shouldTraceMultipleLocalVariableAssignments() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + OrderEvents type1 = domainEvent.getType(); + OrderEvents type2 = type1; + OrderEvents type3 = type2; + service.updateOrderState(type3); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + public OrderEvents getType() { return OrderEvents.RECEIVED; } + } + + enum OrderEvents { RECEIVED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_multiple_assign"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.RECEIVED"); + } + + @Test + void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(BaseEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + interface BaseEvent { + OrderEvents getType(); + } + + abstract class AbstractEvent implements BaseEvent { + public OrderEvents getType() { + return OrderEvents.PAY; + } + } + + // Concrete subclass inherits getType() but doesn't override it! + class InheritingEvent extends AbstractEvent { + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_inherited"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY"); + } + + @Test + void shouldStopTracingRecursiveMethodsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RecursiveEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RecursiveEvent { + public OrderEvents getType() { + return this.getType(); + } + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_recursive"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // Should safely complete without stack overflow and return an empty result + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isEmpty(); + } + + @Test + void shouldHandleChainedMethodCallsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(EventWrapper wrapper) { + service.updateOrderState(wrapper.getEvent().getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class EventWrapper { + public RichEvent getEvent() { return new RichEvent(); } + } + + class RichEvent { + public OrderEvents getType() { return OrderEvents.PAY; } + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_chained"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // It might not fully resolve chained method calls, but it must not crash! + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("wrapper.getEvent().getType()"); + } + + @Test + void shouldTraceStaticFieldReferences() throws IOException { + String source = """ + package com.example; + public class OrderController { + private static final OrderEvents CONSTANT_EVENT = OrderEvents.CANCELLED; + private OrderService service; + public void processOrderEvent() { + service.updateOrderState(CONSTANT_EVENT); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_static_field"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); + } + + @Test + void shouldHandleAnonymousInnerClassesGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + RichEvent event = new RichEvent() { + public OrderEvents getType() { + return OrderEvents.CREATE; + } + }; + service.updateOrderState(event.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + interface RichEvent { + OrderEvents getType(); + } + + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_anonymous"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + 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()"); + } + + @Test + void shouldLinkCallsInsideLambdasToEnclosingMethod() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + Runnable r = () -> service.updateOrderState(OrderEvents.PAY); + r.run(); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_lambda_scope"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.PAY"); + } + + @Test + void shouldHandleTernaryOperatorsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(boolean flag) { + OrderEvents event = flag ? OrderEvents.PAY : OrderEvents.CANCELLED; + service.updateOrderState(event); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY, CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_ternary"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // It should gracefully fallback to the string representation of the ternary operator + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("flag ? OrderEvents.PAY : OrderEvents.CANCELLED"); + } + + @Test + void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + OrderEvents event; + try { + event = OrderEvents.PAY; + } catch (Exception e) { + event = OrderEvents.CANCELLED; + } + service.updateOrderState(event); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY, CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_try_catch"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + 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"); + } + + @Test + void shouldTraceArrayAndCollectionAccessGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY}; + service.updateOrderState(events[0]); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_array"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + 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]"); } } diff --git a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json index cc1d788..f44e11f 100644 --- a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null }, { "event" : "PLACE_ORDER", "className" : "click.kamil.examples.enterprise.service.OrderServiceImpl", @@ -17,7 +18,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, { "event" : "CANCEL_ORDER", "className" : "click.kamil.examples.enterprise.service.OrderServiceImpl", @@ -26,7 +28,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 21 + "lineNumber" : 21, + "polymorphicEvents" : null }, { "event" : "PAY_ORDER", "className" : "click.kamil.examples.enterprise.service.ReactivePaymentService", @@ -35,7 +38,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null }, { "event" : "SHIP_ORDER", "className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener", @@ -44,7 +48,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null }, { "event" : "RETURN_ORDER", "className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener", @@ -53,7 +58,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -185,7 +191,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -219,7 +226,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 21 + "lineNumber" : 21, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -248,7 +256,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : null @@ -278,7 +287,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -312,7 +322,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -346,7 +357,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 17 + "lineNumber" : 17, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json index b6523ec..52eca42 100644 --- a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 34 + "lineNumber" : 34, + "polymorphicEvents" : null }, { "event" : "AUDIT_EVENT", "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", @@ -17,7 +18,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, { "event" : "EXTERNAL_TRIGGER", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -26,7 +28,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 19 + "lineNumber" : 19, + "polymorphicEvents" : null }, { "event" : "SUBMIT_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -35,7 +38,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 20 + "lineNumber" : 20, + "polymorphicEvents" : null }, { "event" : "CANCEL_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -44,7 +48,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : null }, { "event" : "[LIFECYCLE:RESTORE]", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -53,7 +58,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 29 + "lineNumber" : 29, + "polymorphicEvents" : null }, { "event" : "REACTIVE_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", @@ -62,7 +68,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -160,7 +167,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 19 + "lineNumber" : 19, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -190,7 +198,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 20 + "lineNumber" : 20, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -220,7 +229,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -250,7 +260,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 34 + "lineNumber" : 34, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -284,7 +295,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 29 + "lineNumber" : 29, + "polymorphicEvents" : null }, "contextMachineId" : "orderId", "matchedTransitions" : null @@ -314,7 +326,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -343,7 +356,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json index 8f7d7fb..a213249 100644 --- a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 34 + "lineNumber" : 34, + "polymorphicEvents" : null }, { "event" : "AUDIT_EVENT", "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", @@ -17,7 +18,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, { "event" : "EXTERNAL_TRIGGER", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -26,7 +28,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 19 + "lineNumber" : 19, + "polymorphicEvents" : null }, { "event" : "SUBMIT_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -35,7 +38,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 20 + "lineNumber" : 20, + "polymorphicEvents" : null }, { "event" : "CANCEL_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -44,7 +48,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : null }, { "event" : "[LIFECYCLE:RESTORE]", "className" : "click.kamil.examples.statemachine.extended.service.OrderService", @@ -53,7 +58,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 29 + "lineNumber" : 29, + "polymorphicEvents" : null }, { "event" : "REACTIVE_EVENT", "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", @@ -62,7 +68,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -160,7 +167,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 19 + "lineNumber" : 19, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -190,7 +198,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 20 + "lineNumber" : 20, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -220,7 +229,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -250,7 +260,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 34 + "lineNumber" : 34, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -284,7 +295,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 29 + "lineNumber" : 29, + "polymorphicEvents" : null }, "contextMachineId" : "orderId", "matchedTransitions" : null @@ -314,7 +326,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -343,7 +356,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 16 + "lineNumber" : 16, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json index f5658b4..f6e7641 100644 --- a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 15 + "lineNumber" : 15, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -55,7 +56,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 15 + "lineNumber" : 15, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json b/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json index 529e472..20af568 100644 --- a/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json +++ b/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 40 + "lineNumber" : 40, + "polymorphicEvents" : null }, { "event" : "ORDER_EVENT", "className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService", @@ -17,7 +18,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 52 + "lineNumber" : 52, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -103,7 +105,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 40 + "lineNumber" : 40, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json index 5ba76a3..b7f3445 100644 --- a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -67,7 +68,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 18 + "lineNumber" : 18, + "polymorphicEvents" : null }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json index 9ef51be..75d0842 100644 --- a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json @@ -8,7 +8,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : null }, { "event" : "eventProvider", "className" : "click.kamil.service.StateMachineServiceImpl", @@ -17,7 +18,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 50 + "lineNumber" : 50, + "polymorphicEvents" : null }, { "event" : "customMessage", "className" : "click.kamil.service.StateMachineServiceImpl", @@ -26,7 +28,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 78 + "lineNumber" : 78, + "polymorphicEvents" : null } ], "entryPoints" : [ { "type" : "REST", @@ -140,7 +143,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -170,7 +174,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -200,7 +205,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -234,7 +240,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -268,7 +275,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 78 + "lineNumber" : 78, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -302,7 +310,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 25 + "lineNumber" : 25, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -336,7 +345,8 @@ "sourceModule" : null, "stateMachineId" : null, "sourceState" : null, - "lineNumber" : 50 + "lineNumber" : 50, + "polymorphicEvents" : [ ] }, "contextMachineId" : null, "matchedTransitions" : [ {