From 27005da6785bf6c87e3c5e5cc91d68d2c8b096f1 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 5 Jul 2026 16:15:52 +0200 Subject: [PATCH] update number 6 --- .../matching/StrictFqnMatchingEngine.java | 4 - .../service/AbstractCallGraphEngine.java | 74 +++++++++-- .../analysis/service/CallGraphPathFinder.java | 24 ++++ .../analysis/service/EnterpriseBugsTest.java | 124 ++++++++++++++++++ .../EnterpriseStateMachineConfig.json | 70 ++++++++++ .../InheritanceStateMachineConfig.json | 33 +++++ .../MavenOrderStateMachine.json | 74 +++++++++++ .../OrderStateMachineConfig.json | 37 ++++++ 8 files changed, 424 insertions(+), 16 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java index 0162fe8..c323dea 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java @@ -109,10 +109,6 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine { eventStr.equals("payload")) { return true; } - - if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) { - return true; - } if (eventStr.contains(" ? ") && eventStr.contains(" : ")) { return true; 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 5578cc7..2961431 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 @@ -1410,21 +1410,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - List impls = context.getImplementations(className); - if (impls.isEmpty()) { + TypeDeclaration td = context.getTypeDeclaration(className); + if (isFunctionalInterface(className, td)) { allResolved.add(baseCalled); } else { - TypeDeclaration td = context.getTypeDeclaration(className); - boolean isAbstractOrInterface = false; - if (td != null) { - isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); - } - boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression; - if (!isAbstractOrInterface || isImplicitThis) { + List impls = context.getImplementations(className); + if (impls.isEmpty()) { allResolved.add(baseCalled); - } - for (String impl : impls) { - allResolved.add(impl + "." + methodName); + } else { + boolean isAbstractOrInterface = false; + if (td != null) { + isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); + } + boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression; + if (!isAbstractOrInterface || isImplicitThis) { + allResolved.add(baseCalled); + } + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } } } } else { @@ -1434,6 +1438,52 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return allResolved; } + private boolean isFunctionalInterface(String className, TypeDeclaration td) { + if (className == null) return false; + if (className.equals("java.lang.Runnable") || className.equals("Runnable")) return true; + if (className.equals("java.util.concurrent.Callable") || className.equals("Callable")) return true; + if (className.startsWith("java.util.function.")) return true; + + if (td != null && td.isInterface()) { + if (td.modifiers() != null) { + for (Object mod : td.modifiers()) { + if (mod instanceof Annotation ann) { + String name = ann.getTypeName().toString(); + if (name.equals("FunctionalInterface") || name.endsWith(".FunctionalInterface")) { + return true; + } + } + } + } + int abstractMethodCount = 0; + for (MethodDeclaration md : td.getMethods()) { + if (md.getBody() == null) { + String name = md.getName().getIdentifier(); + if (name.equals("equals") && md.parameters().size() == 1) continue; + if (name.equals("hashCode") && md.parameters().isEmpty()) continue; + if (name.equals("toString") && md.parameters().isEmpty()) continue; + + boolean isStaticOrDefault = false; + if (md.modifiers() != null) { + for (Object mod : md.modifiers()) { + if (mod instanceof Modifier m) { + if (m.isStatic() || m.isDefault()) { + isStaticOrDefault = true; + break; + } + } + } + } + if (!isStaticOrDefault) { + abstractMethodCount++; + } + } + } + return abstractMethodCount == 1; + } + return false; + } + private String unwrapConverterMethods(String arg) { if (arg == null) return null; String current = arg; 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 b4ffa8b..782af4f 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 @@ -102,6 +102,30 @@ public class CallGraphPathFinder { } } + // Handle implementation fallback for interfaces/superclasses (interface -> concrete class) + if (result.isEmpty() && start.contains(".")) { + String className = start.substring(0, start.lastIndexOf('.')); + String methodName = start.substring(start.lastIndexOf('.') + 1); + if (className.contains("<")) { + className = className.substring(0, className.indexOf('<')); + } + List impls = context.getImplementations(className); + if (impls != null) { + for (String impl : impls) { + String implMethod = impl + "." + methodName; + if (graph.containsKey(implMethod)) { + List> implPaths = findAllPaths(implMethod, target, graph, visited); + for (List implPath : implPaths) { + List path = new ArrayList<>(); + path.add(start); + path.addAll(implPath); + result.add(path); + } + } + } + } + } + visited.remove(start); return result; } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java index 74819d2..6e86f6b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java @@ -168,4 +168,128 @@ class EnterpriseBugsTest { .build(); assertThat(engine.isRoutedToCorrectMachine(chain2, "com.example.OrderStateMachineConfig", context)).isFalse(); } + + @Test + void shouldFindPathsUsingInterfaceImplementationFallback(@TempDir Path tempDir) throws IOException { + String apiSrc = """ + package com.example; + public interface OrderApi { + void submit(); + } + """; + String controllerSrc = """ + package com.example; + public class OrderController implements OrderApi { + private OrderService service; + public void submit() { + service.trigger(); + } + } + """; + String serviceSrc = """ + package com.example; + public class OrderService { + public void trigger() {} + } + """; + + Files.writeString(tempDir.resolve("OrderApi.java"), apiSrc); + Files.writeString(tempDir.resolve("OrderController.java"), controllerSrc); + Files.writeString(tempDir.resolve("OrderService.java"), serviceSrc); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + var graph = engine.buildCallGraph(); + + CallGraphPathFinder pathFinder = new CallGraphPathFinder(context); + List> paths = pathFinder.findAllPaths("com.example.OrderApi.submit", "com.example.OrderService.trigger", graph, new java.util.HashSet<>()); + + assertThat(paths).hasSize(1); + assertThat(paths.get(0)).containsExactly( + "com.example.OrderApi.submit", + "com.example.OrderController.submit", + "com.example.OrderService.trigger" + ); + } + + @Test + void shouldNotPolymorphicallyExpandFunctionalInterfaceCalls(@TempDir Path tempDir) throws IOException { + String taskSrc = """ + package com.example; + @FunctionalInterface + public interface MyTask { + void run(); + } + """; + String spuriousSrc = """ + package com.example; + public class SpuriousTask implements MyTask { + private UnrelatedService service; + public void run() { + service.doSomething(); + } + } + """; + String unrelatedSrc = """ + package com.example; + public class UnrelatedService { + public void doSomething() {} + } + """; + String executorSrc = """ + package com.example; + public class ExecutorService { + public void execute(MyTask task) { + task.run(); + } + } + """; + String mainSrc = """ + package com.example; + public class MainService { + private ExecutorService executor; + public void runJob() { + executor.execute(() -> {}); + } + } + """; + + Files.writeString(tempDir.resolve("MyTask.java"), taskSrc); + Files.writeString(tempDir.resolve("SpuriousTask.java"), spuriousSrc); + Files.writeString(tempDir.resolve("UnrelatedService.java"), unrelatedSrc); + Files.writeString(tempDir.resolve("ExecutorService.java"), executorSrc); + Files.writeString(tempDir.resolve("MainService.java"), mainSrc); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + var graph = engine.buildCallGraph(); + + // The execute method calls task.run(). Since MyTask is a functional interface, + // it should NOT resolve task.run() to SpuriousTask.run() + var edges = graph.get("com.example.ExecutorService.execute"); + if (edges != null) { + for (var edge : edges) { + assertThat(edge.getTargetMethod()).isNotEqualTo("com.example.SpuriousTask.run"); + } + } + } + + @Test + void shouldNotTreatValueOfExpressionAsWildcardMatchingAllTransitions() { + click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine engine = + new click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine(); + + click.kamil.springstatemachineexporter.model.Event smEvent = + click.kamil.springstatemachineexporter.model.Event.of("PAY", "com.example.OrderEvents.PAY"); + + TriggerPoint triggerPoint = TriggerPoint.builder() + .event("OrderEvents.valueOf(someVar)") + .build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); + } } 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 6ea1064..6e7073a 100644 --- a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json @@ -182,6 +182,76 @@ } ] } ], "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/enterprise/orders/place", + "className" : "click.kamil.examples.enterprise.api.OrderApi", + "methodName" : "placeOrder", + "sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderApi.java", + "metadata" : { + "path" : "/api/enterprise/orders/place", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.enterprise.api.OrderApi.placeOrder", "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ], + "triggerPoint" : { + "event" : "PLACE_ORDER", + "className" : "click.kamil.examples.enterprise.service.OrderServiceImpl", + "methodName" : "place", + "sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 16, + "polymorphicEvents" : [ "PLACE_ORDER" ], + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "NEW", + "targetState" : "CHECK_AVAILABILITY", + "event" : "PLACE_ORDER" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/enterprise/orders/{id}/cancel", + "className" : "click.kamil.examples.enterprise.api.OrderApi", + "methodName" : "cancelOrder", + "sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderApi.java", + "metadata" : { + "path" : "/api/enterprise/orders/{id}/cancel", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "id", + "type" : "String", + "annotations" : [ "PathVariable" ] + } ] + }, + "methodChain" : [ "click.kamil.examples.enterprise.api.OrderApi.cancelOrder", "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ], + "triggerPoint" : { + "event" : "CANCEL_ORDER", + "className" : "click.kamil.examples.enterprise.service.OrderServiceImpl", + "methodName" : "cancel", + "sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 21, + "polymorphicEvents" : [ "CANCEL_ORDER" ], + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "PAID", + "targetState" : "CANCELLED", + "event" : "CANCEL_ORDER" + } ] + }, { "entryPoint" : { "type" : "REST", "name" : "POST /api/enterprise/orders/place", 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 7b9d220..9835d38 100644 --- a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json @@ -37,6 +37,39 @@ "parameters" : [ ] } ], "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/v2/orders/submit", + "className" : "click.kamil.examples.statemachine.inheritance.api.OrderApi", + "methodName" : "submitOrder", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/api/OrderApi.java", + "metadata" : { + "path" : "/api/v2/orders/submit", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.inheritance.api.OrderApi.submitOrder", "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl.submitOrder", "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl.doProcess" ], + "triggerPoint" : { + "event" : "INHERITED_SUBMIT", + "className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl", + "methodName" : "doProcess", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/service/ProcessingServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 15, + "polymorphicEvents" : [ "INHERITED_SUBMIT" ], + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "START", + "targetState" : "WORKING", + "event" : "INHERITED_SUBMIT" + } ] + }, { "entryPoint" : { "type" : "REST", "name" : "POST /api/v2/orders/submit", 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 80fb57e..a9d4ccb 100644 --- a/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json +++ b/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.json @@ -120,6 +120,80 @@ "targetState" : "BUSY", "event" : "SUBMIT" } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /maven/orders/submit", + "className" : "click.kamil.maven.api.MavenOrderApi", + "methodName" : "submitOrder", + "sourceFile" : "api-module/src/main/java/click/kamil/maven/api/MavenOrderApi.java", + "metadata" : { + "path" : "/maven/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, + "methodChain" : [ "click.kamil.maven.api.MavenOrderApi.submitOrder", "click.kamil.maven.core.MavenOrderStateMachine.OrderController.submitOrder" ], + "triggerPoint" : { + "event" : "SUBMIT", + "className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController", + "methodName" : "submitOrder", + "sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 40, + "polymorphicEvents" : null, + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "INIT", + "targetState" : "BUSY", + "event" : "SUBMIT" + } ] + }, { + "entryPoint" : { + "type" : "JMS", + "name" : "JMS: order.queue", + "className" : "click.kamil.maven.api.JmsOrderListener", + "methodName" : "onMessage", + "sourceFile" : "api-module/src/main/java/click/kamil/maven/api/JmsOrderListener.java", + "metadata" : { + "protocol" : "JMS", + "destination" : "order.queue" + }, + "parameters" : [ { + "name" : "message", + "type" : "String", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.maven.api.JmsOrderListener.onMessage", "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage" ], + "triggerPoint" : { + "event" : "ORDER_EVENT", + "className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService", + "methodName" : "onMessage", + "sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 52, + "polymorphicEvents" : [ "ORDER_EVENT" ], + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "BUSY", + "targetState" : "DONE", + "event" : "ORDER_EVENT" + } ] } ], "properties" : { "default" : { } 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 2989855..b727c4d 100644 --- a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json @@ -81,6 +81,43 @@ "targetState" : "PROCESSING", "event" : "SUBMIT" } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.api.OrderApi", + "methodName" : "submitOrder", + "sourceFile" : "api-module/src/main/java/click/kamil/multi/api/OrderApi.java", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, + "methodChain" : [ "click.kamil.multi.api.OrderApi.submitOrder", "click.kamil.multi.core.OrderController.submitOrder" ], + "triggerPoint" : { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 18, + "polymorphicEvents" : null, + "external" : false, + "constraint" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "NEW", + "targetState" : "PROCESSING", + "event" : "SUBMIT" + } ] } ], "properties" : { "default" : { }