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. */