From d59f4ac166433742b993a5d62091f8fe46ced670 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 20 Jun 2026 06:51:15 +0200 Subject: [PATCH] fix attempt 1 --- TODO.md | 3 +- .../enricher/TransitionLinkerEnricher.java | 2 +- .../analysis/service/CallGraphBuilder.java | 29 ++-- .../TransitionLinkerEnricherTest.java | 8 +- .../service/CallGraphBuilderTest.java | 127 ++++++++++++++++++ 5 files changed, 155 insertions(+), 14 deletions(-) diff --git a/TODO.md b/TODO.md index c620abf..73a3186 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,9 @@ 1.extract from all possible formats (ask chatgpt) -[DONE] json [TODO] xml, maybe yaml 2. [PLAN] Support external dependencies (JARs/compiled classes): - Enable binding resolution in ASTParser (`setResolveBindings(true)`). - Configure ASTParser with project classpath (including JARs). - Refactor `CodebaseContext` to handle resolved bindings, not just local AST nodes. - - Implement tests using external libraries as base classes. \ No newline at end of file + - Implement tests using external libraries as base classes. 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 f9f891b..398c3f4 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 @@ -44,7 +44,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") || triggerEvent.equals("msg") || triggerEvent.equals("message") || - triggerEvent.equals("payload") || triggerEvent.matches(".*\\.get[A-Z].*\\(\\)"); + triggerEvent.equals("payload"); if (isWildcard) { String targetVar = chain.getContextMachineId(); 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 cebfef2..7f3ee03 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 @@ -147,12 +147,21 @@ public class CallGraphBuilder { } 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('(')); + if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) { + int lastDot = resolvedValue.lastIndexOf('.'); + int firstDot = resolvedValue.indexOf('.'); + int openParen = resolvedValue.indexOf('(', lastDot); - // Resolve in the first method in the path where the variable might be declared - for (String methodFqn : path) { + if (lastDot > 0 && openParen > lastDot) { + String varName = resolvedValue.substring(0, firstDot); + if (varName.contains("(")) { + varName = null; + } + String methodName = resolvedValue.substring(lastDot + 1, openParen); + + if (varName != null) { + // 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); @@ -187,6 +196,8 @@ public class CallGraphBuilder { break; } } + } + } } if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { @@ -541,9 +552,11 @@ public class CallGraphBuilder { 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); + if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) { + Expression firstArg = (Expression) cic.arguments().get(0); + String resolved = constantResolver.resolve(firstArg, context); + if (resolved != null) { + expr = firstArg; // Only unwrap if it's actually a constant } } 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 d3600af..904adaf 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 @@ -143,15 +143,17 @@ class TransitionLinkerEnricherTest { assertThat(updatedChain.getMatchedTransitions()).hasSize(1); } + + @Test - void shouldMatchMethodCallWildcard() { + void shouldNotMatchArbitraryGetXMethodWildcard() { Transition t1 = new Transition(); t1.setSourceStates(List.of(State.of("NEW", "NEW"))); t1.setTargetStates(List.of(State.of("PAID", "PAID"))); t1.setEvent(Event.of("PAY", "PAY")); CallChain chain = CallChain.builder() - .triggerPoint(TriggerPoint.builder().event("richEvent.getType()").build()) + .triggerPoint(TriggerPoint.builder().event("richEvent.getId()").build()) .contextMachineId("testMachine") .build(); @@ -164,7 +166,7 @@ class TransitionLinkerEnricherTest { enricher.enrich(result, null, null); CallChain updatedChain = result.getMetadata().getCallChains().get(0); - assertThat(updatedChain.getMatchedTransitions()).hasSize(1); + assertThat(updatedChain.getMatchedTransitions()).isNull(); } @Test 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 9db6d32..35b55a8 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 @@ -954,4 +954,131 @@ class CallGraphBuilderTest { // It returns the array access expression as a string. assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]"); } + + @Test + void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(MyEvent event) { + OrderEvents eventType = wrap1(wrap2(wrap3(event.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 MyEvent { + public OrderEvents getType() { return OrderEvents.CREATE; } + } + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_nested_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); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE"); + } + + @Test + void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void createOrder(String oID) { + service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID))); + } + } + class OrderService { + public void processOrderEvent(OrderCancelledEvent event) { } + } + class OrderCancelledEvent { + public OrderCancelledEvent(OrderId id) {} + } + class OrderId { + public OrderId(String id) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_complex_cic"); + 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("createOrder").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))"); + } + + @Test + void shouldResolveMethodReturnConstantFromMultiplePackagesForFix3() throws IOException { + String source1 = """ + package com.example.api; + public interface RichEvent { + String getType(); + } + """; + String source2 = """ + package com.example.impl.a; + import com.example.api.RichEvent; + public class EventA implements RichEvent { + public String getType() { return "EVENT_A"; } + } + """; + String source3 = """ + package com.example.impl.b; + import com.example.api.RichEvent; + public class EventB implements RichEvent { + public String getType() { return "EVENT_B"; } + } + """; + String source4 = """ + package com.example.controller; + import com.example.api.RichEvent; + public class EventController { + private com.example.service.EventService service; + public void handleEvent(RichEvent event) { + service.process(event.getType()); + } + } + """; + String source5 = """ + package com.example.service; + public class EventService { + public void process(String type) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_multi_pkg"); + Files.writeString(tempDir.resolve("RichEvent.java"), source1); + Files.writeString(tempDir.resolve("EventA.java"), source2); + Files.writeString(tempDir.resolve("EventB.java"), source3); + Files.writeString(tempDir.resolve("EventController.java"), source4); + Files.writeString(tempDir.resolve("EventService.java"), source5); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); + } }