2 Commits

Author SHA1 Message Date
2e83af0f33 fix attempt 1 2026-06-20 06:51:15 +02:00
19547d3c5f better html tooltip styling 2026-06-20 06:16:12 +02:00
7 changed files with 175 additions and 13 deletions

View File

@@ -1,6 +1,5 @@
1.extract from all possible formats (ask chatgpt) 1.extract from all possible formats (ask chatgpt)
[DONE] json
[TODO] xml, maybe yaml [TODO] xml, maybe yaml
2. [PLAN] Support external dependencies (JARs/compiled classes): 2. [PLAN] Support external dependencies (JARs/compiled classes):

View File

@@ -44,7 +44,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") || boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
triggerEvent.equals("msg") || triggerEvent.equals("message") || triggerEvent.equals("msg") || triggerEvent.equals("message") ||
triggerEvent.equals("payload") || triggerEvent.matches(".*\\.get[A-Z].*\\(\\)"); triggerEvent.equals("payload") || triggerEvent.matches(".*\\.getType\\(\\)");
if (isWildcard) { if (isWildcard) {
String targetVar = chain.getContextMachineId(); String targetVar = chain.getContextMachineId();

View File

@@ -541,9 +541,11 @@ public class CallGraphBuilder {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
} }
if (expr instanceof ClassInstanceCreation cic) { if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
if (!cic.arguments().isEmpty()) { Expression firstArg = (Expression) cic.arguments().get(0);
expr = (Expression) cic.arguments().get(0); String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
} }
} }

View File

@@ -167,6 +167,30 @@ class TransitionLinkerEnricherTest {
assertThat(updatedChain.getMatchedTransitions()).hasSize(1); assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
} }
@Test
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.getId()").build())
.contextMachineId("testMachine")
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNull();
}
@Test @Test
void shouldAllowWildcardWhenContextMachineIdIsMissing() { void shouldAllowWildcardWhenContextMachineIdIsMissing() {
Transition t1 = new Transition(); Transition t1 = new Transition();

View File

@@ -954,4 +954,131 @@ class CallGraphBuilderTest {
// It returns the array access expression as a string. // It returns the array access expression as a string.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]"); 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<CallChain> 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<CallChain> 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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
}
} }

View File

@@ -238,10 +238,27 @@
.tooltip-content { .tooltip-content {
padding: 20px; padding: 20px;
max-width: 500px; max-width: 500px;
max-height: 60vh;
overflow-y: auto;
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* Custom Scrollbar for Tooltips */
.tooltip-content::-webkit-scrollbar {
width: 6px;
}
.tooltip-content::-webkit-scrollbar-track {
background: transparent;
}
.tooltip-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.tooltip-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.payload-box { .payload-box {
background: #0f172a; background: #0f172a;
color: #e2e8f0; color: #e2e8f0;

View File

@@ -81,13 +81,6 @@ public class HtmlExporterTest {
// 7. Assertions - Surgical SVG Identification // 7. Assertions - Surgical SVG Identification
// Every event should be wrapped in an identifying hyperlink for robust JS selection // Every event should be wrapped in an identifying hyperlink for robust JS selection
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_html.html"), html);
click.kamil.springstatemachineexporter.exporter.ExportOptions plantUmlOptions = click.kamil.springstatemachineexporter.exporter.ExportOptions.builder()
.embedIdentifiers(true)
.build();
click.kamil.springstatemachineexporter.exporter.PlantUml pumlExporter = new click.kamil.springstatemachineexporter.exporter.PlantUml();
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_puml.puml"), pumlExporter.export(result, plantUmlOptions));
assertThat(html).contains("__CREATE"); assertThat(html).contains("__CREATE");
assertThat(html).contains("__ASSIGN"); assertThat(html).contains("__ASSIGN");