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 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() {} +}