From 56cdf96f2dfe57f10e2f2023fa26d45216e490da Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 20 Jun 2026 12:21:11 +0200 Subject: [PATCH] more fixes 3 --- .../enricher/TransitionLinkerEnricher.java | 44 ++++++- .../analysis/service/CallGraphBuilder.java | 56 +++++++- .../TransitionLinkerEnricherTest.java | 39 ++++++ .../service/CallGraphBuilderTest.java | 120 ++++++++++++++++++ 4 files changed, 253 insertions(+), 6 deletions(-) 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 8d3ee3b..4267849 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 @@ -123,17 +123,19 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { result.setMetadata(updatedMetadata); } + private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) { - // If the chain's target expression or qualifier indicates a specific machine name, verify it + // First, check explicit variable targeting (like contextMachineId) String targetVar = chain.getContextMachineId(); if (targetVar == null && chain.getTriggerPoint() != null) { targetVar = chain.getTriggerPoint().getStateMachineId(); } + String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : ""; + if (targetVar != null && !targetVar.isEmpty()) { targetVar = targetVar.toLowerCase(); // E.g., if target is "myStateMachine", ensure current machine name contains "my" - String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase(); // If the variable name ends with StateMachine, we extract the prefix if (targetVar.endsWith("statemachine")) { String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length()); @@ -142,6 +144,44 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { } } } + + + // Second, filter by vertical routing based on Service class name + if (chain.getMethodChain() != null) { + boolean hasElectronicsService = false; + boolean hasComputerStoreService = false; + + for (String method : chain.getMethodChain()) { + String className = method; + if (method.contains(".")) { + className = method.substring(0, method.lastIndexOf('.')); + if (className.contains(".")) { + className = className.substring(className.lastIndexOf('.') + 1); + } + } + + String lowerClass = className.toLowerCase(); + if (lowerClass.contains("electronics")) { + hasElectronicsService = true; + } else if (lowerClass.contains("computerstore")) { + hasComputerStoreService = true; + } + } + + String lowerMachine = currentMachineName != null ? currentMachineName.toLowerCase() : ""; + boolean isElectronicsMachine = lowerMachine.contains("electronics"); + // If the machine is an electronics machine, it must NOT be triggered by computer store + if (isElectronicsMachine && hasComputerStoreService && !hasElectronicsService) { + return false; + } + + // If the machine is NOT an electronics machine (e.g. standard computer store SM), + // it must NOT be triggered by electronics services + if (!isElectronicsMachine && hasElectronicsService && !hasComputerStoreService) { + return false; + } + } + return true; } 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 0db191c..7caad8b 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 @@ -188,8 +188,12 @@ public class CallGraphBuilder { if (declaredType != null) { System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType); List typesToInspect = new ArrayList<>(); - typesToInspect.add(declaredType); - typesToInspect.addAll(context.getImplementations(declaredType)); + if ("inline-instantiation".equals(sourceMethod)) { + typesToInspect.add(declaredType); + } else { + typesToInspect.add(declaredType); + typesToInspect.addAll(context.getImplementations(declaredType)); + } System.out.println("DEEP TRACE IMPLS: " + typesToInspect); for (String type : typesToInspect) { @@ -340,7 +344,12 @@ public class CallGraphBuilder { } else if (retExpr instanceof QualifiedName qn) { constants.add(qn.toString()); } else if (retExpr instanceof SimpleName sn) { - constants.add(sn.toString()); + List consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); + if (!consts.isEmpty()) { + constants.addAll(consts); + } else { + constants.add(sn.toString()); + } } } } @@ -803,4 +812,43 @@ public class CallGraphBuilder { } return expr; } -} + + private List traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set visited) { + List results = new ArrayList<>(); + for (MethodDeclaration md : td.getMethods()) { + if (md.isConstructor() && md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(Assignment node) { + Expression left = node.getLeftHandSide(); + if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) || + (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) { + String val = constantResolver.resolve(node.getRightHandSide(), context); + if (val != null) results.add(val); + } + return super.visit(node); + } + @Override + public boolean visit(SuperConstructorInvocation node) { + for (Object argObj : node.arguments()) { + Expression arg = (Expression) argObj; + String val = constantResolver.resolve(arg, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); + } + } else { + results.add(val); + } + } + } + return super.visit(node); + } + }); + } + } + return results; + } + +} \ No newline at end of file diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java index a14bc36..a8813c5 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java @@ -218,4 +218,43 @@ class TransitionLinkerEnricherTest { assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty(); } + + @Test + void shouldFilterCrossStateMachineRoutingByVertical() { + Transition t1 = new Transition(); + t1.setSourceStates(List.of(State.of("NEW", "NEW"))); + t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED"))); + t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE")); + + CallChain chain = CallChain.builder() + .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build()) + .methodChain(List.of( + "com.example.ElectronicsOrderController.post()", + "com.example.ElectronicsOrderService.processOrderEvent()" + )) + .build(); + + // Testing against Electronics SM - should match + AnalysisResult resultElectronics = AnalysisResult.builder() + .name("ElectronicsOwnDeliveryStateMachineConfiguration") + .transitions(List.of(t1)) + .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build()) + .build(); + + enricher.enrich(resultElectronics, null, null); + CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0); + assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1); + + // Testing against ComputerStore SM - should NOT match because chain has Electronics service + AnalysisResult resultComputer = AnalysisResult.builder() + .name("OwnDeliveryStateMachineConfiguration") // the non-electronics one + .transitions(List.of(t1)) + .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build()) + .build(); + + enricher.enrich(resultComputer, null, null); + CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0); + assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty(); + } + } \ No newline at end of file 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 b6377a0..a737f6f 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 @@ -1520,4 +1520,124 @@ class CallGraphBuilderTest { org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS"); } + + @Test + void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + service.process(new BaseEvent().getEvent()); + } + } + + class BaseEvent { + public String getEvent() { return "BASE_EVENT_RETURN"; } + } + + class SubEvent extends BaseEvent { + public String getEvent() { return "SUB_EVENT_RETURN"; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass"); + 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("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + // Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN"); + } + + + @Test + void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + service.process(new OrderPickedUpEvent().getType()); + } + } + + class OrderPickedUpEvent { + private String type; + public OrderPickedUpEvent() { + this.type = "PICKED_UP_IN_CONSTRUCTOR"; + } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_field_const"); + 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("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").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("PICKED_UP_IN_CONSTRUCTOR"); + } + + @Test + void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + service.process(new OrderCancelledEvent().getType()); + } + } + + abstract class BaseEvent { + private String type; + public BaseEvent(String type, String otherVar) { + this.type = type; + } + public String getType() { return type; } + } + + class OrderCancelledEvent extends BaseEvent { + public OrderCancelledEvent() { + super("CANCELLED_IN_SUPER", "someVar"); + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_super_const"); + 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("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").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()).contains("CANCELLED_IN_SUPER"); + } + } \ No newline at end of file