diff --git a/server/pom.xml b/server/pom.xml
index cc6d090..4c9a8ff 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -172,7 +172,7 @@
io.github.liquid-java
liquidjava-fsm
- 0.2.2
+ 0.2.3
tools.aqua
diff --git a/server/src/main/java/dtos/errors/StateRefinementErrorDTO.java b/server/src/main/java/dtos/errors/StateRefinementErrorDTO.java
index 0940d97..e994a45 100644
--- a/server/src/main/java/dtos/errors/StateRefinementErrorDTO.java
+++ b/server/src/main/java/dtos/errors/StateRefinementErrorDTO.java
@@ -1,15 +1,22 @@
package dtos.errors;
-import java.io.File;
+import java.util.Collection;
import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
import dtos.diagnostics.SourcePositionDTO;
import dtos.diagnostics.VCSimplificationResultDTO;
import dtos.fsm.StateMachineDTO;
import liquidjava.diagnostics.errors.StateErrorContext;
import liquidjava.diagnostics.errors.StateRefinementError;
+import liquidjava.fsm.StateMachine;
import liquidjava.fsm.StateMachineParser;
+import liquidjava.processor.context.ContextHistory;
+import liquidjava.processor.context.GhostState;
+import liquidjava.processor.context.RefinedFunction;
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
+import utils.PathUtils;
/**
* DTO for serializing StateRefinementError instances to JSON
@@ -28,14 +35,37 @@ public StateRefinementErrorDTO(StateRefinementError error) {
this.found = VCSimplificationResultDTO.from(error.getFoundSimplification());
this.customMessage = error.getCustomMessage();
this.declarationPosition = SourcePositionDTO.from(error.getDeclarationPosition());
- StateErrorContext context = error.getContext();
this.stateMachines = declarationPosition == null || declarationPosition.file() == null ? List.of()
- : StateMachineDTO.from(StateMachineParser.parseAll(
- new File(declarationPosition.file()).toURI().toString(),
- context == null ? null : context.receiverClass()), context);
+ : stateMachines(declarationPosition.file(), specFiles(), error.getContext());
}
public static StateRefinementErrorDTO from(StateRefinementError error) {
return new StateRefinementErrorDTO(error);
}
+
+ /**
+ * The diagrams of the receiver's class, which may inherit the state sets and methods of its supertypes' specs
+ * @param declarationFile the file of the spec of the called method
+ * @param specFiles the files of the other specs, where those of the supertypes may be
+ * @param context the state error, or null if the verifier gives no states, in which case the diagrams are those of
+ * the first class of the declaration's file
+ */
+ static List stateMachines(String declarationFile, Collection specFiles,
+ StateErrorContext context) {
+ String declarationUri = PathUtils.toFileUri(declarationFile);
+ List sms = context == null ? StateMachineParser.parseAll(declarationUri)
+ : StateMachineParser.parseAll(Stream.concat(Stream.of(declarationUri),
+ specFiles.stream().map(PathUtils::toFileUri)).toList(), context.receiverClass());
+ return StateMachineDTO.from(sms, context);
+ }
+
+ /** The files of the state sets and state refinements of the last verification */
+ static List specFiles() {
+ ContextHistory history = ContextHistory.getInstance();
+ Stream stateSets = history.getGhosts().stream().map(GhostState::getFile);
+ Stream stateRefinements = history.getMethods().stream().filter(RefinedFunction::hasStateChange)
+ .map(method -> SourcePositionDTO.from(method.getPlacementInCode().getPosition()))
+ .filter(Objects::nonNull).map(SourcePositionDTO::file);
+ return Stream.concat(stateSets, stateRefinements).filter(Objects::nonNull).distinct().toList();
+ }
}
diff --git a/server/src/test/java/dtos/errors/StateRefinementErrorDTOTest.java b/server/src/test/java/dtos/errors/StateRefinementErrorDTOTest.java
new file mode 100644
index 0000000..6836529
--- /dev/null
+++ b/server/src/test/java/dtos/errors/StateRefinementErrorDTOTest.java
@@ -0,0 +1,74 @@
+package dtos.errors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import dtos.fsm.StateMachineDTO;
+import liquidjava.api.CommandLineLauncher;
+import liquidjava.diagnostics.errors.StateErrorContext;
+import liquidjava.fsm.StateMachineTransition;
+
+class StateRefinementErrorDTOTest {
+
+ private static final Path HIERARCHY = Path.of("src", "test", "resources", "hierarchy").toAbsolutePath();
+ private static final String WINDOW = file("A_WindowRefinements.java");
+ private static final String FRAME = file("B_FrameRefinements.java");
+ private static final String JFRAME = file("C_JFrameRefinements.java");
+
+ private static String file(String name) {
+ return HIERARCHY.resolve(name).toString();
+ }
+
+ private static StateErrorContext call(String receiverClass, String method) {
+ return new StateErrorContext("host", receiverClass, method, List.of(List.of("displayable")),
+ List.of(List.of("notDisplayable")), true);
+ }
+
+ private static List methods(StateMachineDTO sm) {
+ return sm.transitions().stream().map(StateMachineTransition::label).distinct().toList();
+ }
+
+ @Test
+ void showsTheCallOnTheStateSetItsClassInheritsFromTheSuperclassSpec() {
+ List dtos = StateRefinementErrorDTO.stateMachines(FRAME, List.of(WINDOW),
+ call("java.awt.Frame", "setUndecorated(boolean)"));
+ assertEquals(1, dtos.size());
+ StateMachineDTO sm = dtos.get(0);
+ assertEquals("java.awt.Frame", sm.className());
+ assertEquals(List.of("displayable", "notDisplayable"), sm.states());
+ assertEquals(List.of("setUndecorated", "dispose", "pack"), methods(sm));
+ assertEquals(List.of("displayable"), sm.errorContext().invalidStates());
+ assertEquals(List.of("notDisplayable"), sm.errorContext().acceptedStates());
+ }
+
+ @Test
+ void showsTheMethodsOfASuperclassSpecWithoutStateSet() {
+ List dtos = StateRefinementErrorDTO.stateMachines(JFRAME, List.of(WINDOW, FRAME),
+ call("javax.swing.JFrame", "setDefaultCloseOperation(int)"));
+ assertEquals(List.of("setDefaultCloseOperation", "setUndecorated", "dispose", "pack"), methods(dtos.get(0)));
+ }
+
+ @Test
+ void showsTheDeclarationFilesFirstClassWithoutTheVerifiersStates() {
+ List dtos = StateRefinementErrorDTO.stateMachines(WINDOW, List.of(FRAME), null);
+ assertEquals(List.of("java.awt.Window"), dtos.stream().map(StateMachineDTO::className).toList());
+ assertNull(dtos.get(0).errorContext());
+ }
+
+ @Test
+ void readsTheSpecsOfTheStateSetsAndOfTheMethodsWithoutStateSet() {
+ boolean lspMode = CommandLineLauncher.cmdArgs.lspMode;
+ CommandLineLauncher.cmdArgs.lspMode = true; // the verifier only keeps its context history in the IDE
+ try {
+ CommandLineLauncher.launch(HIERARCHY.toString());
+ } finally {
+ CommandLineLauncher.cmdArgs.lspMode = lspMode;
+ }
+ assertTrue(StateRefinementErrorDTO.specFiles().containsAll(List.of(WINDOW, FRAME, JFRAME)),
+ StateRefinementErrorDTO.specFiles().toString());
+ }
+}
diff --git a/server/src/test/resources/hierarchy/A_WindowRefinements.java b/server/src/test/resources/hierarchy/A_WindowRefinements.java
new file mode 100644
index 0000000..f7dc9b8
--- /dev/null
+++ b/server/src/test/resources/hierarchy/A_WindowRefinements.java
@@ -0,0 +1,14 @@
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// the files are named so that the verifier reads the superclasses' specs first (liquidjava#461)
+@StateSet({"displayable", "notDisplayable"})
+@ExternalRefinementsFor("java.awt.Window")
+interface WindowRefinements {
+ @StateRefinement(to = "notDisplayable(this)")
+ void dispose();
+
+ @StateRefinement(to = "displayable(this)")
+ void pack();
+}
diff --git a/server/src/test/resources/hierarchy/B_FrameRefinements.java b/server/src/test/resources/hierarchy/B_FrameRefinements.java
new file mode 100644
index 0000000..542559f
--- /dev/null
+++ b/server/src/test/resources/hierarchy/B_FrameRefinements.java
@@ -0,0 +1,12 @@
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+
+// the state set is declared in the superclass's spec, in another file
+@ExternalRefinementsFor("java.awt.Frame")
+interface FrameRefinements {
+ @StateRefinement(to = "notDisplayable(this)")
+ void Frame(String title);
+
+ @StateRefinement(from = "notDisplayable(this)")
+ void setUndecorated(boolean undecorated);
+}
diff --git a/server/src/test/resources/hierarchy/C_JFrameRefinements.java b/server/src/test/resources/hierarchy/C_JFrameRefinements.java
new file mode 100644
index 0000000..5744173
--- /dev/null
+++ b/server/src/test/resources/hierarchy/C_JFrameRefinements.java
@@ -0,0 +1,12 @@
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+
+// inherits the state set from Window's spec and setUndecorated from Frame's, which has no state set
+@ExternalRefinementsFor("javax.swing.JFrame")
+interface JFrameRefinements {
+ @StateRefinement(to = "notDisplayable(this)")
+ void JFrame(String title);
+
+ @StateRefinement(from = "notDisplayable(this)")
+ void setDefaultCloseOperation(int operation);
+}
diff --git a/server/src/test/resources/hierarchy/Use.java b/server/src/test/resources/hierarchy/Use.java
new file mode 100644
index 0000000..d1bfb64
--- /dev/null
+++ b/server/src/test/resources/hierarchy/Use.java
@@ -0,0 +1,9 @@
+import java.awt.Frame;
+
+class Use {
+ static void borderless() {
+ Frame host = new Frame("Playfield");
+ host.pack();
+ host.setUndecorated(true);
+ }
+}