diff --git a/server/pom.xml b/server/pom.xml
index d94dd9f..57af6c8 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -6,6 +6,12 @@
vscode-liquid-java-server
0.0.1-SNAPSHOT
+ src/test/java
+
+
+ src/test/resources
+
+
net.revelc.code.formatter
@@ -207,6 +213,24 @@
antlr4-runtime
4.7.1
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.9.3
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.9.3
+ test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ 1.9.3
+ test
+
diff --git a/server/src/main/java/fsm/StateMachineParser.java b/server/src/main/java/fsm/StateMachineParser.java
index adb78f0..628a0e5 100644
--- a/server/src/main/java/fsm/StateMachineParser.java
+++ b/server/src/main/java/fsm/StateMachineParser.java
@@ -2,6 +2,7 @@
import java.net.URI;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -13,8 +14,7 @@
import spoon.reflect.CtModel;
import spoon.reflect.declaration.CtAnnotation;
import spoon.reflect.declaration.CtClass;
-import spoon.reflect.declaration.CtConstructor;
-import spoon.reflect.declaration.CtInterface;
+import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
@@ -41,32 +41,21 @@ public static StateMachine parse(String uri) {
// get class or interface
CtType> ctType = getType(model);
- if (ctType == null) {
- return null;
- }
+ if (ctType == null)
+ return null; // no class or interface found
// extract class name and states
List states = getStates(ctType);
- if (states == null || states.isEmpty()) {
- return null;
- }
-
+ if (states == null || states.isEmpty())
+ return null; // no states found
String className = getClassName(ctType);
- // extract initial state and transitions
- List initialStates;
- List transitions;
- if (ctType instanceof CtClass> ctClass) {
- initialStates = getInitialStatesFromClass(ctClass, states);
- transitions = getTransitionsFromClass(ctClass, states);
- } else if (ctType instanceof CtInterface> ctInterface) {
- initialStates = getInitialStatesFromInterface(ctInterface, className, states);
- transitions = getTransitionsFromInterface(ctInterface, className, states);
- } else {
- return null;
- }
- if (transitions.isEmpty()) return null; // no transitions found
-
+ // get initial states and transitions
+ List initialStates = getInitialStates(ctType, className, states);
+ List transitions = getTransitions(ctType, className, states);
+ if (transitions.isEmpty())
+ return null; // no transitions found
+
return new StateMachine(className, initialStates, states, transitions);
} catch (Exception e) {
@@ -82,9 +71,8 @@ public static StateMachine parse(String uri) {
*/
private static CtType> getType(CtModel model) {
for (CtType> type : model.getAllTypes()) {
- if (type instanceof CtClass> || type instanceof CtInterface>) {
+ if (type.isClass() || type.isInterface())
return type;
- }
}
return null;
}
@@ -99,7 +87,7 @@ private static String getClassName(CtType> ctType) {
for (CtAnnotation> annotation : ctType.getAnnotations()) {
if (annotation.getAnnotationType().getSimpleName().equals(EXTERNAL_REFINEMENTS_FOR_ANNOTATION)) {
String qualifiedName = (String) annotation.getValueAsObject("value");
- return Utils.getSimpleName(qualifiedName);
+ return Utils.getSimpleName(qualifiedName);
}
}
return ctType.getSimpleName();
@@ -121,80 +109,54 @@ private static List getStates(CtType> ctType) {
}
/**
- * Gets the initial states from a class
- * If not explicitely defined, uses the first state in the state set
- * @param ctClass the CtClass
- * @param states the list of states
- * @return initial states
+ * Gets the elements that represent constructors (actual constructors for classes, methods named after the class for interfaces)
+ * @param ctType the CtType (class or interface)
+ * @param className the class name
+ * @return collection of constructor elements
*/
- private static List getInitialStatesFromClass(CtClass> ctClass, List states) {
- Set initialStates = new HashSet<>();
- for (CtConstructor> constructor : ctClass.getConstructors()) {
- for (CtAnnotation> annotation : constructor.getAnnotations()) {
- if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
- String to = annotation.getValueAsString("to");
- List parsedStates = parseStateExpression(to, states);
- initialStates.addAll(parsedStates);
- }
- }
+ private static Collection extends CtElement> getConstructorElements(CtType> ctType, String className) {
+ if (ctType instanceof CtClass> ctClass) {
+ return ctClass.getConstructors();
}
- return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
+ // for interfaces the constructors are methods with the same name as the class
+ return ctType.getMethods().stream().filter(m -> m.getSimpleName().equals(className)).toList();
}
/**
- * Gets the initial state from an interface
- * If not explicitely defined, uses the first state in the state set
- * @param ctInterface the CtInterface
+ * Gets the initial states from a class or interface
+ * If not explicitly defined, uses the first state in the state set
+ * @param ctType the CtType (class or interface)
* @param className the class name
+ * @param states the list of states
* @return initial states
*/
- private static List getInitialStatesFromInterface(CtInterface> ctInterface, String className, List states) {
+ private static List getInitialStates(CtType> ctType, String className, List states) {
Set initialStates = new HashSet<>();
- for (CtMethod> method : ctInterface.getMethods()) {
- if (method.getSimpleName().equals(className)) {
- for (CtAnnotation> annotation : method.getAnnotations()) {
- if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
- String to = annotation.getValueAsString("to");
- List parsedStates = parseStateExpression(to, states);
- initialStates.addAll(parsedStates);
- }
- }
- }
- }
- return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
- }
-
- /**
- * Gets transitions from a class
- * @param ctClass the CtClass
- * @param states the list of states
- * @return list of StateMachineTransition
- */
- private static List getTransitionsFromClass(CtClass> ctClass, List states) {
- List transitions = new ArrayList<>();
- for (CtMethod> method : ctClass.getMethods()) {
- for (CtAnnotation> annotation : method.getAnnotations()) {
+ for (CtElement element : getConstructorElements(ctType, className)) {
+ for (CtAnnotation> annotation : element.getAnnotations()) {
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
- List extracted = getTransitions(annotation, method.getSimpleName(), states);
- transitions.addAll(extracted);
+ String to = annotation.getValueAsString("to");
+ List parsedStates = parseStateExpression(to, states);
+ initialStates.addAll(parsedStates);
}
}
}
-
- return transitions;
+ return initialStates.isEmpty() ? List.of(states.get(0)) : initialStates.stream().toList();
}
/**
- * Gets transitions from an interface
- * @param ctInterface the CtInterface
+ * Gets transitions from a class or interface
+ * @param ctType the CtType (class or interface)
* @param className the class name
* @param states the list of states
* @return list of StateMachineTransition
*/
- private static List getTransitionsFromInterface(CtInterface> ctInterface, String className, List states) {
+ private static List getTransitions(CtType> ctType, String className, List states) {
List transitions = new ArrayList<>();
- for (CtMethod> method : ctInterface.getMethods()) {
- if (method.getSimpleName().equals(className)) continue; // skip constructor method
+ for (CtMethod> method : ctType.getMethods()) {
+ // for interfaces we skip constructor methods (methods with same name as class)
+ if (ctType.isInterface() && method.getSimpleName().equals(className))
+ continue;
for (CtAnnotation> annotation : method.getAnnotations()) {
if (annotation.getAnnotationType().getSimpleName().equals(STATE_REFINEMENT_ANNOTATION)) {
@@ -261,36 +223,34 @@ private static List parseStateExpression(String expr, List state
*/
private static List getStateExpressions(Expression expr, List states) {
List stateExpressions = new ArrayList<>();
- switch (expr) {
- case Var var -> stateExpressions.add(var.getName());
- case FunctionInvocation func -> stateExpressions.add(func.getName());
- case GroupExpression group -> stateExpressions.addAll(getStateExpressions(group.getExpression(), states));
- case BinaryExpression bin -> {
- String op = bin.getOperator();
- if (op.equals("||")) {
- // combine states from both operands
- stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states));
- stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states));
- }
+ if (expr instanceof Var var) {
+ stateExpressions.add(var.getName());
+ } else if (expr instanceof FunctionInvocation func) {
+ stateExpressions.add(func.getName());
+ } else if (expr instanceof GroupExpression group) {
+ stateExpressions.addAll(getStateExpressions(group.getExpression(), states));
+ } else if (expr instanceof BinaryExpression bin) {
+ String op = bin.getOperator();
+ if (op.equals("||")) {
+ // combine states from both operands
+ stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states));
+ stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states));
}
- case UnaryExpression unary -> {
- if (unary.getOp().equals("!")) {
- // all except those in the expression
- List negatedStates = getStateExpressions(unary.getExpression(), states);
- for (String state : states) {
- if (!negatedStates.contains(state)) {
- stateExpressions.add(state);
- }
+ } else if (expr instanceof UnaryExpression unary) {
+ if (unary.getOp().equals("!")) {
+ // all except those in the expression
+ List negatedStates = getStateExpressions(unary.getExpression(), states);
+ for (String state : states) {
+ if (!negatedStates.contains(state)) {
+ stateExpressions.add(state);
}
}
}
- case Ite ite -> {
- // combine states from then and else branches
- // TODO: handle conditional transitions
- stateExpressions.addAll(getStateExpressions(ite.getThen(), states));
- stateExpressions.addAll(getStateExpressions(ite.getElse(), states));
- }
- default -> {}
+ } else if (expr instanceof Ite ite) {
+ // combine states from then and else branches
+ // TODO: handle conditional transitions
+ stateExpressions.addAll(getStateExpressions(ite.getThen(), states));
+ stateExpressions.addAll(getStateExpressions(ite.getElse(), states));
}
return stateExpressions;
}
diff --git a/server/src/test/java/fsm/StateMachineParserTests.java b/server/src/test/java/fsm/StateMachineParserTests.java
new file mode 100644
index 0000000..27a4266
--- /dev/null
+++ b/server/src/test/java/fsm/StateMachineParserTests.java
@@ -0,0 +1,102 @@
+package fsm;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.lang.Thread.State;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class StateMachineParserTests {
+
+ private static final String BASE_URI = "src/test/resources/fsm/";
+
+ @Test
+ public void testSimpleStateMachine() {
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "Simple.java");
+ StateMachine expectedSm = new StateMachine("Simple", List.of("open"), List.of("open", "closed"),
+ List.of(new StateMachineTransition("open", "closed", "close"),
+ new StateMachineTransition("open", "open", "read")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testOrTransition() {
+ // state1 || state2 => separate transitions from both state1 and state2
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "OrTransition.java");
+ StateMachine expectedSm = new StateMachine("OrTransition", List.of("a"), List.of("a", "b", "c"), List
+ .of(new StateMachineTransition("a", "c", "action"), new StateMachineTransition("b", "c", "action")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testNegationTransition() {
+ // !state => all states except state
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "NegationTransition.java");
+ StateMachine expectedSm = new StateMachine("NegationTransition", List.of("open"),
+ List.of("open", "closed", "locked"), List.of(new StateMachineTransition("open", "locked", "lock"),
+ new StateMachineTransition("closed", "locked", "lock")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testSelfLoop() {
+ // from=state, to=state or from=state => self-loop
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "SelfLoop.java");
+ StateMachine expectedSm = new StateMachine("SelfLoop", List.of("idle"), List.of("idle", "running"),
+ List.of(new StateMachineTransition("idle", "idle", "noop"),
+ new StateMachineTransition("idle", "running", "start"),
+ new StateMachineTransition("running", "running", "tick")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testToOnlyTransition() {
+ // no from => all states contain a transition to state
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "ToOnlyTransition.java");
+ StateMachine expectedSm = new StateMachine("ToOnlyTransition", List.of("a"), List.of("a", "b", "c"),
+ List.of(new StateMachineTransition("a", "c", "action"), new StateMachineTransition("b", "c", "action"),
+ new StateMachineTransition("c", "c", "action")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testMultipleInitialStates() {
+ // overloading constructors with different initial states
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "MultipleInitialStates.java");
+ StateMachine expectedSm = new StateMachine("MultipleInitialStates", List.of("initialized", "uninitialized"),
+ List.of("initialized", "uninitialized", "error"),
+ List.of(new StateMachineTransition("uninitialized", "initialized", "init")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testExternalRefinementsInterface() {
+ // class name from @ExternalStateRefinements
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "ExternalRefinements.java");
+ StateMachine expectedSm = new StateMachine("Connection", List.of("disconnected"),
+ List.of("connected", "disconnected"),
+ List.of(new StateMachineTransition("disconnected", "connected", "connect")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ @Test
+ public void testConditionalTransition() {
+ // transitions for both branches of condition
+ StateMachine sm = StateMachineParser.parse(BASE_URI + "ConditionalTransition.java");
+ StateMachine expectedSm = new StateMachine("ConditionalTransition", List.of("off", "on"), List.of("on", "off"),
+ List.of(new StateMachineTransition("on", "off", "turnOff"),
+ new StateMachineTransition("off", "on", "turnOn")));
+ assertStateMachineEquals(expectedSm, sm);
+ }
+
+ private static void assertStateMachineEquals(StateMachine expected, StateMachine actual) {
+ assertNotNull(actual, "State machine should not be null");
+ assertEquals(expected.className(), actual.className(), "Class names should match");
+ assertEquals(expected.initialStates(), actual.initialStates(), "Initial states should match");
+ assertEquals(expected.states(), actual.states(), "States should match");
+ assertEquals(expected.transitions(), actual.transitions(), "State transitions should match");
+ }
+}
diff --git a/server/src/test/resources/fsm/ConditionalTransition.java b/server/src/test/resources/fsm/ConditionalTransition.java
new file mode 100644
index 0000000..2e41d35
--- /dev/null
+++ b/server/src/test/resources/fsm/ConditionalTransition.java
@@ -0,0 +1,18 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"on", "off"})
+public class ConditionalTransition {
+
+ // include transitions of both branches
+ @StateRefinement(to="flag ? on(this) : off(this)")
+ public ConditionalTransition(boolean flag) {}
+
+ @StateRefinement(from="off(this)", to="on(this)")
+ public void turnOn() {}
+
+ @StateRefinement(from="on(this)", to="off(this)")
+ public void turnOff() {}
+}
diff --git a/server/src/test/resources/fsm/ExternalRefinements.java b/server/src/test/resources/fsm/ExternalRefinements.java
new file mode 100644
index 0000000..e348341
--- /dev/null
+++ b/server/src/test/resources/fsm/ExternalRefinements.java
@@ -0,0 +1,16 @@
+package fsm;
+
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@ExternalRefinementsFor("com.example.Connection")
+@StateSet({"connected", "disconnected"})
+public interface ExternalRefinements {
+
+ @StateRefinement(to="disconnected(this)")
+ void Connection();
+
+ @StateRefinement(from="disconnected(this)", to="connected(this)")
+ void connect();
+}
diff --git a/server/src/test/resources/fsm/MultipleInitialStates.java b/server/src/test/resources/fsm/MultipleInitialStates.java
new file mode 100644
index 0000000..4f9b39c
--- /dev/null
+++ b/server/src/test/resources/fsm/MultipleInitialStates.java
@@ -0,0 +1,17 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"initialized", "uninitialized", "error"})
+public class MultipleInitialStates {
+
+ @StateRefinement(to="uninitialized(this)")
+ public MultipleInitialStates() {}
+
+ @StateRefinement(to="initialized(this)")
+ public MultipleInitialStates(int code) {}
+
+ @StateRefinement(from="uninitialized(this)", to="initialized(this)")
+ public void init(int code) {}
+}
diff --git a/server/src/test/resources/fsm/NegationTransition.java b/server/src/test/resources/fsm/NegationTransition.java
new file mode 100644
index 0000000..a1b6c0f
--- /dev/null
+++ b/server/src/test/resources/fsm/NegationTransition.java
@@ -0,0 +1,15 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"open", "closed", "locked"})
+public class NegationTransition {
+
+ @StateRefinement(to="open(this)")
+ public NegationTransition() {}
+
+ // transition from all states except "locked" to "locked"
+ @StateRefinement(from="!locked(this)", to="locked(this)")
+ public void lock() {}
+}
diff --git a/server/src/test/resources/fsm/OrTransition.java b/server/src/test/resources/fsm/OrTransition.java
new file mode 100644
index 0000000..c96df8d
--- /dev/null
+++ b/server/src/test/resources/fsm/OrTransition.java
@@ -0,0 +1,15 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"a", "b", "c"})
+public class OrTransition {
+
+ @StateRefinement(to="a(this)")
+ public OrTransition() {}
+
+ // transition from both a and b to c
+ @StateRefinement(from="a(this) || b(this)", to="c(this)")
+ public void action() {}
+}
diff --git a/server/src/test/resources/fsm/SelfLoop.java b/server/src/test/resources/fsm/SelfLoop.java
new file mode 100644
index 0000000..5a3c745
--- /dev/null
+++ b/server/src/test/resources/fsm/SelfLoop.java
@@ -0,0 +1,23 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"idle", "running"})
+public class SelfLoop {
+
+ @StateRefinement(to="idle(this)")
+ public SelfLoop() {}
+
+ // explicit self-loop
+ @StateRefinement(from="running(this)", to="running(this)")
+ public void tick() {}
+
+ // implicit self-loop
+ @StateRefinement(from="idle(this)")
+ public void noop() {}
+
+ // transition to another state
+ @StateRefinement(from="idle(this)", to="running(this)")
+ public void start() {}
+}
diff --git a/server/src/test/resources/fsm/Simple.java b/server/src/test/resources/fsm/Simple.java
new file mode 100644
index 0000000..389e3e1
--- /dev/null
+++ b/server/src/test/resources/fsm/Simple.java
@@ -0,0 +1,17 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"open", "closed"})
+public class Simple {
+
+ @StateRefinement(to="open(this)")
+ public Simple() {}
+
+ @StateRefinement(from="open(this)")
+ public void read() {}
+
+ @StateRefinement(from="open(this)", to="closed(this)")
+ public void close() {}
+}
diff --git a/server/src/test/resources/fsm/ToOnlyTransition.java b/server/src/test/resources/fsm/ToOnlyTransition.java
new file mode 100644
index 0000000..5f6dc16
--- /dev/null
+++ b/server/src/test/resources/fsm/ToOnlyTransition.java
@@ -0,0 +1,15 @@
+package fsm;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"a", "b", "c"})
+public class ToOnlyTransition {
+
+ @StateRefinement(to="a(this)")
+ public ToOnlyTransition() {}
+
+ // all transitions to state c
+ @StateRefinement(to="c(this)")
+ public void action() {}
+}