Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<artifactId>vscode-liquid-java-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<testSourceDirectory>src/test/java</testSourceDirectory>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>net.revelc.code.formatter</groupId>
Expand Down Expand Up @@ -207,6 +213,24 @@
<artifactId>antlr4-runtime</artifactId>
<version>4.7.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
170 changes: 65 additions & 105 deletions server/src/main/java/fsm/StateMachineParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String> 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<String> initialStates;
List<StateMachineTransition> 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<String> initialStates = getInitialStates(ctType, className, states);
List<StateMachineTransition> transitions = getTransitions(ctType, className, states);
if (transitions.isEmpty())
return null; // no transitions found

return new StateMachine(className, initialStates, states, transitions);

} catch (Exception e) {
Expand All @@ -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;
}
Expand All @@ -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();
Expand All @@ -121,80 +109,54 @@ private static List<String> 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<String> getInitialStatesFromClass(CtClass<?> ctClass, List<String> states) {
Set<String> 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<String> 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<String> getInitialStatesFromInterface(CtInterface<?> ctInterface, String className, List<String> states) {
private static List<String> getInitialStates(CtType<?> ctType, String className, List<String> states) {
Set<String> 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<String> 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<StateMachineTransition> getTransitionsFromClass(CtClass<?> ctClass, List<String> states) {
List<StateMachineTransition> 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<StateMachineTransition> extracted = getTransitions(annotation, method.getSimpleName(), states);
transitions.addAll(extracted);
String to = annotation.getValueAsString("to");
List<String> 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<StateMachineTransition> getTransitionsFromInterface(CtInterface<?> ctInterface, String className, List<String> states) {
private static List<StateMachineTransition> getTransitions(CtType<?> ctType, String className, List<String> states) {
List<StateMachineTransition> 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)) {
Expand Down Expand Up @@ -261,36 +223,34 @@ private static List<String> parseStateExpression(String expr, List<String> state
*/
private static List<String> getStateExpressions(Expression expr, List<String> states) {
List<String> 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<String> 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<String> 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;
}
Expand Down
102 changes: 102 additions & 0 deletions server/src/test/java/fsm/StateMachineParserTests.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading