diff --git a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java
index 1ca84c3..7528055 100644
--- a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java
+++ b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java
@@ -140,7 +140,9 @@ public class HtmlExporter implements StateMachineExporter {
}
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
LinkResolution resolution = chain.getLinkResolution();
- return resolution == null || resolution == LinkResolution.RESOLVED;
+ return resolution == null
+ || resolution == LinkResolution.RESOLVED
+ || resolution == LinkResolution.AMBIGUOUS_WIDEN;
}
return false;
}
diff --git a/state_machine_exporter_html/src/main/resources/html/template.html b/state_machine_exporter_html/src/main/resources/html/template.html
index dfe6d97..71cc5db 100644
--- a/state_machine_exporter_html/src/main/resources/html/template.html
+++ b/state_machine_exporter_html/src/main/resources/html/template.html
@@ -377,12 +377,21 @@
return c.linkResolution === 'RESOLVED' || c.linkResolution == null;
}
+ function isAmbiguousWidenChain(c) {
+ return c.linkResolution === 'AMBIGUOUS_WIDEN'
+ && c.matchedTransitions && c.matchedTransitions.length > 0;
+ }
+
+ function isHighlightableChain(c) {
+ return isResolvedChain(c) || isAmbiguousWidenChain(c);
+ }
+
function isUnresolvedExternalChain(c) {
return c.linkResolution === 'UNRESOLVED_EXTERNAL';
}
function isSidebarChain(c) {
- return isLifecycleChain(c) || isResolvedChain(c) || isUnresolvedExternalChain(c);
+ return isLifecycleChain(c) || isHighlightableChain(c) || isUnresolvedExternalChain(c);
}
function chainsForEntryPoint(ep) {
@@ -397,6 +406,10 @@
return chainsForEntryPoint(ep).filter(isResolvedChain);
}
+ function highlightableChainsForEntryPoint(ep) {
+ return chainsForEntryPoint(ep).filter(isHighlightableChain);
+ }
+
function populateSidebar() {
const list = document.getElementById('ep-list');
if (!list) return;
@@ -421,12 +434,15 @@
if (lifecycleChain) {
const label = formatLifecycleLabel(lifecycleChain.triggerPoint.event);
triggersHtml += `${label}`;
- } else if (chains.some(isUnresolvedExternalChain) && !chains.some(isResolvedChain)) {
+ } else if (chains.some(isUnresolvedExternalChain) && !chains.some(isHighlightableChain)) {
triggersHtml += `Unresolved external`;
} else {
+ if (chains.some(isAmbiguousWidenChain)) {
+ triggersHtml += `Ambiguous link`;
+ }
triggersHtml += 'Triggers: ';
const addedEvents = new Set();
- chains.filter(isResolvedChain).forEach(c => {
+ chains.filter(isHighlightableChain).forEach(c => {
c.matchedTransitions.forEach(t => {
const evName = t.event || "";
if (!addedEvents.has(evName)) {
@@ -470,7 +486,7 @@
function highlightEntryPoint(ep) {
clearHighlights();
- const chains = resolvedChainsForEntryPoint(ep);
+ const chains = highlightableChainsForEntryPoint(ep);
const seen = new Set();
let steps = [];
@@ -479,7 +495,12 @@
const key = buildLinkKey(t.sourceState, t.event);
if (!key || seen.has(key)) return;
seen.add(key);
- steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
+ steps.push({
+ event: t.event,
+ source: t.sourceState,
+ ambiguous: c.linkResolution === 'AMBIGUOUS_WIDEN'
+ || (c.triggerPoint && c.triggerPoint.ambiguous)
+ });
});
});
diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlTransitionLinkConsistencyTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlTransitionLinkConsistencyTest.java
index 3368f53..dfd0831 100644
--- a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlTransitionLinkConsistencyTest.java
+++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlTransitionLinkConsistencyTest.java
@@ -283,6 +283,12 @@ class HtmlTransitionLinkConsistencyTest {
.className("com.example.web.OrderController")
.methodName("suspend")
.build();
+ EntryPoint ambiguousEp = EntryPoint.builder()
+ .type(EntryPoint.Type.CUSTOM)
+ .name("ambiguous")
+ .className("com.example.web.AmbiguousController")
+ .methodName("trigger")
+ .build();
CallChain resolvedChain = CallChain.builder()
.entryPoint(resolvedEp)
@@ -318,12 +324,7 @@ class HtmlTransitionLinkConsistencyTest {
.build())
.build();
CallChain ambiguousChain = CallChain.builder()
- .entryPoint(EntryPoint.builder()
- .type(EntryPoint.Type.CUSTOM)
- .name("ambiguous")
- .className("com.example.web.AmbiguousController")
- .methodName("trigger")
- .build())
+ .entryPoint(ambiguousEp)
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
.triggerPoint(TriggerPoint.builder()
.event("com.example.order.OrderEvent.PAY")
@@ -347,7 +348,7 @@ class HtmlTransitionLinkConsistencyTest {
.startStates(Set.of("com.example.order.OrderState.NEW"))
.endStates(Set.of("com.example.order.OrderState.PAID"))
.metadata(CodebaseMetadata.builder()
- .entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp))
+ .entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp, ambiguousEp))
.callChains(List.of(resolvedChain, unresolvedChain, noMatchChain, ambiguousChain))
.build())
.build();
@@ -355,13 +356,63 @@ class HtmlTransitionLinkConsistencyTest {
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
assertThat(html).contains("function isResolvedChain(c)");
+ assertThat(html).contains("function isAmbiguousWidenChain(c)");
+ assertThat(html).contains("function isHighlightableChain(c)");
assertThat(html).contains("function isSidebarChain(c)");
- assertThat(html).contains("function resolvedChainsForEntryPoint(ep)");
+ assertThat(html).contains("function highlightableChainsForEntryPoint(ep)");
+ assertThat(html).contains("Ambiguous link");
+ assertThat(html).contains("active-path-ambiguous");
assertThat(html).contains("Unresolved external");
- assertThat(html).contains("const chains = resolvedChainsForEntryPoint(ep)");
+ assertThat(html).contains("const chains = highlightableChainsForEntryPoint(ep)");
assertThat(html).contains("linkResolution === 'UNRESOLVED_EXTERNAL'");
assertThat(html).contains("linkResolution === 'RESOLVED'");
+ assertThat(html).contains("linkResolution === 'AMBIGUOUS_WIDEN'");
assertThat(html).contains("id=\"sidebar\"");
+ assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
+ }
+
+ @Test
+ void ambiguousWidenHtmlShouldExposeMatchedTransitionLinkKeys() {
+ EntryPoint ambiguousEp = EntryPoint.builder()
+ .type(EntryPoint.Type.CUSTOM)
+ .name("ambiguous")
+ .className("com.example.web.AmbiguousController")
+ .methodName("trigger")
+ .build();
+
+ AnalysisResult result = AnalysisResult.builder()
+ .name("com.example.config.OrderStateMachineConfiguration")
+ .stateTypeFqn("com.example.order.OrderState")
+ .eventTypeFqn("com.example.order.OrderEvent")
+ .transitions(List.of(shortTransition(
+ "com.example.order.OrderEvent.PAY",
+ "com.example.order.OrderState.NEW",
+ "com.example.order.OrderState.PAID")))
+ .startStates(Set.of("com.example.order.OrderState.NEW"))
+ .endStates(Set.of("com.example.order.OrderState.PAID"))
+ .metadata(CodebaseMetadata.builder()
+ .entryPoints(List.of(ambiguousEp))
+ .callChains(List.of(CallChain.builder()
+ .entryPoint(ambiguousEp)
+ .linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
+ .triggerPoint(TriggerPoint.builder()
+ .event("com.example.order.OrderEvent.PAY")
+ .ambiguous(true)
+ .build())
+ .matchedTransitions(List.of(MatchedTransition.builder()
+ .sourceState("com.example.order.OrderState.NEW")
+ .targetState("com.example.order.OrderState.PAID")
+ .event("com.example.order.OrderEvent.PAY")
+ .build()))
+ .build()))
+ .build())
+ .build();
+
+ String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
+
+ assertThat(html).contains("Ambiguous link");
+ assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
+ assertThat(html).contains("com.example.web.AmbiguousController");
}
/** Mirrors {@code template.html} link-key derivation for regression checks. */