From ad3f886b0442aa4d00b5716acd59cc956be861d0 Mon Sep 17 00:00:00 2001 From: Catarina Gamboa Date: Tue, 27 Oct 2026 11:10:38 +0000 Subject: [PATCH] Draw entry transitions from the initial state on the state machine Bump liquidjava-fsm to 0.2.2, which reports the entryTransitions of a StateRefinement without from beside ones with from: how an object in an unknown state enters the protocol. Co-Authored-By: Claude Opus 5.5 --- client/src/types/fsm.ts | 2 ++ client/src/webview/diagram-focus.spec.ts | 11 +++++++++++ client/src/webview/diagram.ts | 6 ++++++ client/src/webview/views/fsm/fsm.spec.ts | 1 + client/src/webview/views/fsm/fsm.ts | 1 + server/pom.xml | 2 +- server/src/main/java/dtos/fsm/StateMachineDTO.java | 5 ++++- .../src/test/java/dtos/fsm/StateMachineDTOTest.java | 10 ++++++++++ 8 files changed, 36 insertions(+), 2 deletions(-) diff --git a/client/src/types/fsm.ts b/client/src/types/fsm.ts index 9e70bb4..7087289 100644 --- a/client/src/types/fsm.ts +++ b/client/src/types/fsm.ts @@ -6,6 +6,8 @@ export type LJStateMachine = { // index of the state set among the class's @StateSet annotations, in declaration order stateSet?: number; initialTransitions: { to: string; toCondition?: string | null; constructorSignature?: string | null }[]; + // methods that put an object in an unknown state (a field, a parameter or a getter's result) into a state + entryTransitions?: { to: string; label: string; toCondition?: string | null }[]; states: string[]; transitions: { from: string; to: string; label: string; fromCondition?: string | null; toCondition?: string | null }[]; // the state error on this state set, null if the call does not involve it diff --git a/client/src/webview/diagram-focus.spec.ts b/client/src/webview/diagram-focus.spec.ts index 8145072..ededfa6 100644 --- a/client/src/webview/diagram-focus.spec.ts +++ b/client/src/webview/diagram-focus.spec.ts @@ -115,6 +115,17 @@ describe('state error focus', () => { expect(full).not.toMatch(/ljStateDimmed|lj-dimmed/); }); + it('draws entry transitions from the initial pseudo state, dimmed when focused', () => { + // a getter that puts a tree node in an unknown state into maybeHasChildren when it returns false + const sm = { ...treeNode(), entryTransitions: [{ to: 'maybeHasChildren', label: 'isLeaf', toCondition: '!return' }] }; + + expect(lines(createMermaidDiagram(sm, 'TB', false, false))).toContain('[*] --> maybeHasChildren : isLeaf'); + expect(lines(createMermaidDiagram(sm, 'TB', true, false))).toContain( + '[*] --> maybeHasChildren : isLeaf
!return'); + // the constructor and the entry transition come first + expect(dimmedTransitions(createMermaidDiagram(sm, 'TB', false, true))[0]).toMatch(/^%% lj-dimmed-transitions: edge0 edge1\b/); + }); + it('only keeps the failed method and the ways out of the possible states bright', () => { const diagram = lines(createMermaidDiagram(datagramSocket(), 'TB', false, true)); diff --git a/client/src/webview/diagram.ts b/client/src/webview/diagram.ts index 2e93993..d205ea1 100644 --- a/client/src/webview/diagram.ts +++ b/client/src/webview/diagram.ts @@ -56,6 +56,12 @@ export function createMermaidDiagram( const label = getInitialTransitionLabel(transition.constructorSignature, transition.toCondition, showConditions); addTransition(` [*] --> ${transition.to}${label ? ` : ${label}` : ''}`, relatedStates.length > 0); }); + + // entry transitions of objects in an unknown state, also from the initial pseudo state and dimmed in focus mode + sm.entryTransitions?.forEach(transition => { + const label = getTransitionLabel(transition.label, null, transition.toCondition, showConditions); + addTransition(` [*] --> ${transition.to} : ${label}`, relatedStates.length > 0); + }); // in focus mode, only the failed method and the ways out of the possible states stay bright const isUsefulTransition = (from: string, to: string) => !!errorFocus?.possibleStates.includes(from) && from !== to; diff --git a/client/src/webview/views/fsm/fsm.spec.ts b/client/src/webview/views/fsm/fsm.spec.ts index 455acae..039f66d 100644 --- a/client/src/webview/views/fsm/fsm.spec.ts +++ b/client/src/webview/views/fsm/fsm.spec.ts @@ -91,6 +91,7 @@ describe('state machine view', () => { ['initial transition postcondition', machine({ initialTransitions: [{ to: 'Closed', toCondition: 'size == 0' }] })], ['method precondition', machine({ transitions: [{ from: 'Closed', to: 'Open', label: 'open', fromCondition: 'size > 0' }] })], ['method postcondition', machine({ transitions: [{ from: 'Closed', to: 'Open', label: 'open', toCondition: 'size == 1' }] })], + ['entry transition postcondition', machine({ entryTransitions: [{ to: 'Open', label: 'isOpen', toCondition: 'return' }] })], ] as const)('enables condition expansion for an %s', (_, stateMachine) => { renderStateMachineView(document.body, [stateMachine], [diagram], 'TB', false); diff --git a/client/src/webview/views/fsm/fsm.ts b/client/src/webview/views/fsm/fsm.ts index 5e08ae7..8cd7ac2 100644 --- a/client/src/webview/views/fsm/fsm.ts +++ b/client/src/webview/views/fsm/fsm.ts @@ -33,6 +33,7 @@ function renderStateMachineViewHtml(sms: LJStateMachine[], diagrams: string[], o const errorFocuses = getStateErrorFocuses(sms); const hasConditionExpansions = sms.some(sm => sm.initialTransitions.some(transition => !!transition.toCondition) + || sm.entryTransitions?.some(transition => !!transition.toCondition) || sm.transitions.some(transition => !!transition.fromCondition || !!transition.toCondition)); const conditionToggleLabel = !hasConditionExpansions ? 'No additional conditions to expand' diff --git a/server/pom.xml b/server/pom.xml index 735309d..cc6d090 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -172,7 +172,7 @@ io.github.liquid-java liquidjava-fsm - 0.2.1 + 0.2.2 tools.aqua diff --git a/server/src/main/java/dtos/fsm/StateMachineDTO.java b/server/src/main/java/dtos/fsm/StateMachineDTO.java index 67a2121..53c42c7 100644 --- a/server/src/main/java/dtos/fsm/StateMachineDTO.java +++ b/server/src/main/java/dtos/fsm/StateMachineDTO.java @@ -4,6 +4,7 @@ import liquidjava.diagnostics.errors.StateErrorContext; import liquidjava.fsm.StateMachine; +import liquidjava.fsm.StateMachineEntryTransition; import liquidjava.fsm.StateMachineInitialTransition; import liquidjava.fsm.StateMachineTransition; @@ -11,6 +12,7 @@ * DTO for serializing the state machine diagram of one state set, with the state error shown on it * * @param stateSet the index of the state set among the class's StateSet annotations + * @param entryTransitions the methods that put an object in an unknown state into a state of this state set * @param errorContext the state error on this diagram, or null if the call does not involve this state set */ public record StateMachineDTO( @@ -19,12 +21,13 @@ public record StateMachineDTO( List states, List transitions, List initialTransitions, + List entryTransitions, StateErrorContextDTO errorContext) { public static StateMachineDTO from(StateMachine sm, StateErrorContext context) { if (sm == null) return null; return new StateMachineDTO(sm.className(), sm.stateSet(), sm.states(), sm.transitions(), - sm.initialTransitions(), StateErrorContextDTO.from(context, sm.states(), sm.stateSet())); + sm.initialTransitions(), sm.entryTransitions(), StateErrorContextDTO.from(context, sm.states(), sm.stateSet())); } /** diff --git a/server/src/test/java/dtos/fsm/StateMachineDTOTest.java b/server/src/test/java/dtos/fsm/StateMachineDTOTest.java index 3999204..715eecf 100644 --- a/server/src/test/java/dtos/fsm/StateMachineDTOTest.java +++ b/server/src/test/java/dtos/fsm/StateMachineDTOTest.java @@ -8,6 +8,7 @@ import liquidjava.diagnostics.errors.StateErrorContext; import liquidjava.fsm.StateMachine; +import liquidjava.fsm.StateMachineEntryTransition; import liquidjava.fsm.StateMachineInitialTransition; import liquidjava.fsm.StateMachineTransition; @@ -51,6 +52,15 @@ void showsTheErrorOnEveryStateSetWhenNoneHasTheCalledMethod() { assertNotNull(dtos.get(1).errorContext()); } + @Test + void keepsTheEntryTransitions() { + StateMachine sm = new StateMachine("example.Edit", 0, List.of("start", "endDone"), + List.of(new StateMachineTransition("start", "endDone", "end")), + List.of(new StateMachineInitialTransition("start")), + List.of(new StateMachineEntryTransition("endDone", "canUndo", "return"))); + assertEquals(sm.entryTransitions(), StateMachineDTO.from(sm, null).entryTransitions()); + } + @Test void hasNoErrorWithoutAContext() { List dtos = StateMachineDTO.from(List.of(TILING, COMPRESSION), null);