Skip to content

Commit 7c65a0b

Browse files
authored
Refactor State Machine Parser (#50)
1 parent 1cbe97a commit 7c65a0b

File tree

11 files changed

+327
-105
lines changed

11 files changed

+327
-105
lines changed

server/pom.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
<artifactId>vscode-liquid-java-server</artifactId>
77
<version>0.0.1-SNAPSHOT</version>
88
<build>
9+
<testSourceDirectory>src/test/java</testSourceDirectory>
10+
<testResources>
11+
<testResource>
12+
<directory>src/test/resources</directory>
13+
</testResource>
14+
</testResources>
915
<plugins>
1016
<plugin>
1117
<groupId>net.revelc.code.formatter</groupId>
@@ -207,6 +213,24 @@
207213
<artifactId>antlr4-runtime</artifactId>
208214
<version>4.7.1</version>
209215
</dependency>
216+
<dependency>
217+
<groupId>org.junit.jupiter</groupId>
218+
<artifactId>junit-jupiter-api</artifactId>
219+
<version>5.9.3</version>
220+
<scope>test</scope>
221+
</dependency>
222+
<dependency>
223+
<groupId>org.junit.jupiter</groupId>
224+
<artifactId>junit-jupiter-engine</artifactId>
225+
<version>5.9.3</version>
226+
<scope>test</scope>
227+
</dependency>
228+
<dependency>
229+
<groupId>org.junit.platform</groupId>
230+
<artifactId>junit-platform-launcher</artifactId>
231+
<version>1.9.3</version>
232+
<scope>test</scope>
233+
</dependency>
210234

211235
</dependencies>
212236

server/src/main/java/fsm/StateMachineParser.java

Lines changed: 65 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.net.URI;
44
import java.util.ArrayList;
5+
import java.util.Collection;
56
import java.util.HashSet;
67
import java.util.List;
78
import java.util.Set;
@@ -13,8 +14,7 @@
1314
import spoon.reflect.CtModel;
1415
import spoon.reflect.declaration.CtAnnotation;
1516
import spoon.reflect.declaration.CtClass;
16-
import spoon.reflect.declaration.CtConstructor;
17-
import spoon.reflect.declaration.CtInterface;
17+
import spoon.reflect.declaration.CtElement;
1818
import spoon.reflect.declaration.CtMethod;
1919
import spoon.reflect.declaration.CtType;
2020

@@ -41,32 +41,21 @@ public static StateMachine parse(String uri) {
4141

4242
// get class or interface
4343
CtType<?> ctType = getType(model);
44-
if (ctType == null) {
45-
return null;
46-
}
44+
if (ctType == null)
45+
return null; // no class or interface found
4746

4847
// extract class name and states
4948
List<String> states = getStates(ctType);
50-
if (states == null || states.isEmpty()) {
51-
return null;
52-
}
53-
49+
if (states == null || states.isEmpty())
50+
return null; // no states found
5451
String className = getClassName(ctType);
5552

56-
// extract initial state and transitions
57-
List<String> initialStates;
58-
List<StateMachineTransition> transitions;
59-
if (ctType instanceof CtClass<?> ctClass) {
60-
initialStates = getInitialStatesFromClass(ctClass, states);
61-
transitions = getTransitionsFromClass(ctClass, states);
62-
} else if (ctType instanceof CtInterface<?> ctInterface) {
63-
initialStates = getInitialStatesFromInterface(ctInterface, className, states);
64-
transitions = getTransitionsFromInterface(ctInterface, className, states);
65-
} else {
66-
return null;
67-
}
68-
if (transitions.isEmpty()) return null; // no transitions found
69-
53+
// get initial states and transitions
54+
List<String> initialStates = getInitialStates(ctType, className, states);
55+
List<StateMachineTransition> transitions = getTransitions(ctType, className, states);
56+
if (transitions.isEmpty())
57+
return null; // no transitions found
58+
7059
return new StateMachine(className, initialStates, states, transitions);
7160

7261
} catch (Exception e) {
@@ -82,9 +71,8 @@ public static StateMachine parse(String uri) {
8271
*/
8372
private static CtType<?> getType(CtModel model) {
8473
for (CtType<?> type : model.getAllTypes()) {
85-
if (type instanceof CtClass<?> || type instanceof CtInterface<?>) {
74+
if (type.isClass() || type.isInterface())
8675
return type;
87-
}
8876
}
8977
return null;
9078
}
@@ -99,7 +87,7 @@ private static String getClassName(CtType<?> ctType) {
9987
for (CtAnnotation<?> annotation : ctType.getAnnotations()) {
10088
if (annotation.getAnnotationType().getSimpleName().equals(EXTERNAL_REFINEMENTS_FOR_ANNOTATION)) {
10189
String qualifiedName = (String) annotation.getValueAsObject("value");
102-
return Utils.getSimpleName(qualifiedName);
90+
return Utils.getSimpleName(qualifiedName);
10391
}
10492
}
10593
return ctType.getSimpleName();
@@ -121,80 +109,54 @@ private static List<String> getStates(CtType<?> ctType) {
121109
}
122110

123111
/**
124-
* Gets the initial states from a class
125-
* If not explicitely defined, uses the first state in the state set
126-
* @param ctClass the CtClass
127-
* @param states the list of states
128-
* @return initial states
112+
* Gets the elements that represent constructors (actual constructors for classes, methods named after the class for interfaces)
113+
* @param ctType the CtType (class or interface)
114+
* @param className the class name
115+
* @return collection of constructor elements
129116
*/
130-
private static List<String> getInitialStatesFromClass(CtClass<?> ctClass, List<String> states) {
131-
Set<String> initialStates = new HashSet<>();
132-
for (CtConstructor<?> constructor : ctClass.getConstructors()) {
133-
for (CtAnnotation<?> annotation : constructor.getAnnotations()) {
134-
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
135-
String to = annotation.getValueAsString("to");
136-
List<String> parsedStates = parseStateExpression(to, states);
137-
initialStates.addAll(parsedStates);
138-
}
139-
}
117+
private static Collection<? extends CtElement> getConstructorElements(CtType<?> ctType, String className) {
118+
if (ctType instanceof CtClass<?> ctClass) {
119+
return ctClass.getConstructors();
140120
}
141-
return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
121+
// for interfaces the constructors are methods with the same name as the class
122+
return ctType.getMethods().stream().filter(m -> m.getSimpleName().equals(className)).toList();
142123
}
143124

144125
/**
145-
* Gets the initial state from an interface
146-
* If not explicitely defined, uses the first state in the state set
147-
* @param ctInterface the CtInterface
126+
* Gets the initial states from a class or interface
127+
* If not explicitly defined, uses the first state in the state set
128+
* @param ctType the CtType (class or interface)
148129
* @param className the class name
130+
* @param states the list of states
149131
* @return initial states
150132
*/
151-
private static List<String> getInitialStatesFromInterface(CtInterface<?> ctInterface, String className, List<String> states) {
133+
private static List<String> getInitialStates(CtType<?> ctType, String className, List<String> states) {
152134
Set<String> initialStates = new HashSet<>();
153-
for (CtMethod<?> method : ctInterface.getMethods()) {
154-
if (method.getSimpleName().equals(className)) {
155-
for (CtAnnotation<?> annotation : method.getAnnotations()) {
156-
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
157-
String to = annotation.getValueAsString("to");
158-
List<String> parsedStates = parseStateExpression(to, states);
159-
initialStates.addAll(parsedStates);
160-
}
161-
}
162-
}
163-
}
164-
return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
165-
}
166-
167-
/**
168-
* Gets transitions from a class
169-
* @param ctClass the CtClass
170-
* @param states the list of states
171-
* @return list of StateMachineTransition
172-
*/
173-
private static List<StateMachineTransition> getTransitionsFromClass(CtClass<?> ctClass, List<String> states) {
174-
List<StateMachineTransition> transitions = new ArrayList<>();
175-
for (CtMethod<?> method : ctClass.getMethods()) {
176-
for (CtAnnotation<?> annotation : method.getAnnotations()) {
135+
for (CtElement element : getConstructorElements(ctType, className)) {
136+
for (CtAnnotation<?> annotation : element.getAnnotations()) {
177137
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
178-
List<StateMachineTransition> extracted = getTransitions(annotation, method.getSimpleName(), states);
179-
transitions.addAll(extracted);
138+
String to = annotation.getValueAsString("to");
139+
List<String> parsedStates = parseStateExpression(to, states);
140+
initialStates.addAll(parsedStates);
180141
}
181142
}
182143
}
183-
184-
return transitions;
144+
return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
185145
}
186146

187147
/**
188-
* Gets transitions from an interface
189-
* @param ctInterface the CtInterface
148+
* Gets transitions from a class or interface
149+
* @param ctType the CtType (class or interface)
190150
* @param className the class name
191151
* @param states the list of states
192152
* @return list of StateMachineTransition
193153
*/
194-
private static List<StateMachineTransition> getTransitionsFromInterface(CtInterface<?> ctInterface, String className, List<String> states) {
154+
private static List<StateMachineTransition> getTransitions(CtType<?> ctType, String className, List<String> states) {
195155
List<StateMachineTransition> transitions = new ArrayList<>();
196-
for (CtMethod<?> method : ctInterface.getMethods()) {
197-
if (method.getSimpleName().equals(className)) continue; // skip constructor method
156+
for (CtMethod<?> method : ctType.getMethods()) {
157+
// for interfaces we skip constructor methods (methods with same name as class)
158+
if (ctType.isInterface() && method.getSimpleName().equals(className))
159+
continue;
198160

199161
for (CtAnnotation<?> annotation : method.getAnnotations()) {
200162
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
@@ -261,36 +223,34 @@ private static List<String> parseStateExpression(String expr, List<String> state
261223
*/
262224
private static List<String> getStateExpressions(Expression expr, List<String> states) {
263225
List<String> stateExpressions = new ArrayList<>();
264-
switch (expr) {
265-
case Var var -> stateExpressions.add(var.getName());
266-
case FunctionInvocation func -> stateExpressions.add(func.getName());
267-
case GroupExpression group -> stateExpressions.addAll(getStateExpressions(group.getExpression(), states));
268-
case BinaryExpression bin -> {
269-
String op = bin.getOperator();
270-
if (op.equals("||")) {
271-
// combine states from both operands
272-
stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states));
273-
stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states));
274-
}
226+
if (expr instanceof Var var) {
227+
stateExpressions.add(var.getName());
228+
} else if (expr instanceof FunctionInvocation func) {
229+
stateExpressions.add(func.getName());
230+
} else if (expr instanceof GroupExpression group) {
231+
stateExpressions.addAll(getStateExpressions(group.getExpression(), states));
232+
} else if (expr instanceof BinaryExpression bin) {
233+
String op = bin.getOperator();
234+
if (op.equals("||")) {
235+
// combine states from both operands
236+
stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states));
237+
stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states));
275238
}
276-
case UnaryExpression unary -> {
277-
if (unary.getOp().equals("!")) {
278-
// all except those in the expression
279-
List<String> negatedStates = getStateExpressions(unary.getExpression(), states);
280-
for (String state : states) {
281-
if (!negatedStates.contains(state)) {
282-
stateExpressions.add(state);
283-
}
239+
} else if (expr instanceof UnaryExpression unary) {
240+
if (unary.getOp().equals("!")) {
241+
// all except those in the expression
242+
List<String> negatedStates = getStateExpressions(unary.getExpression(), states);
243+
for (String state : states) {
244+
if (!negatedStates.contains(state)) {
245+
stateExpressions.add(state);
284246
}
285247
}
286248
}
287-
case Ite ite -> {
288-
// combine states from then and else branches
289-
// TODO: handle conditional transitions
290-
stateExpressions.addAll(getStateExpressions(ite.getThen(), states));
291-
stateExpressions.addAll(getStateExpressions(ite.getElse(), states));
292-
}
293-
default -> {}
249+
} else if (expr instanceof Ite ite) {
250+
// combine states from then and else branches
251+
// TODO: handle conditional transitions
252+
stateExpressions.addAll(getStateExpressions(ite.getThen(), states));
253+
stateExpressions.addAll(getStateExpressions(ite.getElse(), states));
294254
}
295255
return stateExpressions;
296256
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package fsm;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.lang.Thread.State;
6+
import java.util.List;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
12+
public class StateMachineParserTests {
13+
14+
private static final String BASE_URI = "src/test/resources/fsm/";
15+
16+
@Test
17+
public void testSimpleStateMachine() {
18+
StateMachine sm = StateMachineParser.parse(BASE_URI + "Simple.java");
19+
StateMachine expectedSm = new StateMachine("Simple", List.of("open"), List.of("open", "closed"),
20+
List.of(new StateMachineTransition("open", "closed", "close"),
21+
new StateMachineTransition("open", "open", "read")));
22+
assertStateMachineEquals(expectedSm, sm);
23+
}
24+
25+
@Test
26+
public void testOrTransition() {
27+
// state1 || state2 => separate transitions from both state1 and state2
28+
StateMachine sm = StateMachineParser.parse(BASE_URI + "OrTransition.java");
29+
StateMachine expectedSm = new StateMachine("OrTransition", List.of("a"), List.of("a", "b", "c"), List
30+
.of(new StateMachineTransition("a", "c", "action"), new StateMachineTransition("b", "c", "action")));
31+
assertStateMachineEquals(expectedSm, sm);
32+
}
33+
34+
@Test
35+
public void testNegationTransition() {
36+
// !state => all states except state
37+
StateMachine sm = StateMachineParser.parse(BASE_URI + "NegationTransition.java");
38+
StateMachine expectedSm = new StateMachine("NegationTransition", List.of("open"),
39+
List.of("open", "closed", "locked"), List.of(new StateMachineTransition("open", "locked", "lock"),
40+
new StateMachineTransition("closed", "locked", "lock")));
41+
assertStateMachineEquals(expectedSm, sm);
42+
}
43+
44+
@Test
45+
public void testSelfLoop() {
46+
// from=state, to=state or from=state => self-loop
47+
StateMachine sm = StateMachineParser.parse(BASE_URI + "SelfLoop.java");
48+
StateMachine expectedSm = new StateMachine("SelfLoop", List.of("idle"), List.of("idle", "running"),
49+
List.of(new StateMachineTransition("idle", "idle", "noop"),
50+
new StateMachineTransition("idle", "running", "start"),
51+
new StateMachineTransition("running", "running", "tick")));
52+
assertStateMachineEquals(expectedSm, sm);
53+
}
54+
55+
@Test
56+
public void testToOnlyTransition() {
57+
// no from => all states contain a transition to state
58+
StateMachine sm = StateMachineParser.parse(BASE_URI + "ToOnlyTransition.java");
59+
StateMachine expectedSm = new StateMachine("ToOnlyTransition", List.of("a"), List.of("a", "b", "c"),
60+
List.of(new StateMachineTransition("a", "c", "action"), new StateMachineTransition("b", "c", "action"),
61+
new StateMachineTransition("c", "c", "action")));
62+
assertStateMachineEquals(expectedSm, sm);
63+
}
64+
65+
@Test
66+
public void testMultipleInitialStates() {
67+
// overloading constructors with different initial states
68+
StateMachine sm = StateMachineParser.parse(BASE_URI + "MultipleInitialStates.java");
69+
StateMachine expectedSm = new StateMachine("MultipleInitialStates", List.of("initialized", "uninitialized"),
70+
List.of("initialized", "uninitialized", "error"),
71+
List.of(new StateMachineTransition("uninitialized", "initialized", "init")));
72+
assertStateMachineEquals(expectedSm, sm);
73+
}
74+
75+
@Test
76+
public void testExternalRefinementsInterface() {
77+
// class name from @ExternalStateRefinements
78+
StateMachine sm = StateMachineParser.parse(BASE_URI + "ExternalRefinements.java");
79+
StateMachine expectedSm = new StateMachine("Connection", List.of("disconnected"),
80+
List.of("connected", "disconnected"),
81+
List.of(new StateMachineTransition("disconnected", "connected", "connect")));
82+
assertStateMachineEquals(expectedSm, sm);
83+
}
84+
85+
@Test
86+
public void testConditionalTransition() {
87+
// transitions for both branches of condition
88+
StateMachine sm = StateMachineParser.parse(BASE_URI + "ConditionalTransition.java");
89+
StateMachine expectedSm = new StateMachine("ConditionalTransition", List.of("off", "on"), List.of("on", "off"),
90+
List.of(new StateMachineTransition("on", "off", "turnOff"),
91+
new StateMachineTransition("off", "on", "turnOn")));
92+
assertStateMachineEquals(expectedSm, sm);
93+
}
94+
95+
private static void assertStateMachineEquals(StateMachine expected, StateMachine actual) {
96+
assertNotNull(actual, "State machine should not be null");
97+
assertEquals(expected.className(), actual.className(), "Class names should match");
98+
assertEquals(expected.initialStates(), actual.initialStates(), "Initial states should match");
99+
assertEquals(expected.states(), actual.states(), "States should match");
100+
assertEquals(expected.transitions(), actual.transitions(), "State transitions should match");
101+
}
102+
}

0 commit comments

Comments
 (0)