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);