diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-11-15 02:19:06 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-11-17 12:41:35 +0100 |
commit | ac478b828e5c6dbea72df1303cbe1e0da0fde42e (patch) | |
tree | a91419a2f2a591ab3cc1b06d750db95c817b550d /subprojects | |
parent | fix: scope upper bounds (diff) | |
download | refinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.tar.gz refinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.tar.zst refinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.zip |
feat(language): opposite reference validation
Diffstat (limited to 'subprojects')
15 files changed, 555 insertions, 41 deletions
diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java index 899e3cb3..b4abce81 100644 --- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java +++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java | |||
@@ -77,7 +77,7 @@ class ModelGenerationTest { | |||
77 | % Scope | 77 | % Scope |
78 | scope Post = 5, Person = 5. | 78 | scope Post = 5, Person = 5. |
79 | """); | 79 | """); |
80 | assertThat(parsedProblem.errors(), empty()); | 80 | assertThat(parsedProblem.getResourceErrors(), empty()); |
81 | var problem = parsedProblem.problem(); | 81 | var problem = parsedProblem.problem(); |
82 | 82 | ||
83 | var storeBuilder = ModelStore.builder() | 83 | var storeBuilder = ModelStore.builder() |
@@ -211,7 +211,7 @@ class ModelGenerationTest { | |||
211 | 211 | ||
212 | scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. | 212 | scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. |
213 | """); | 213 | """); |
214 | assertThat(parsedProblem.errors(), empty()); | 214 | assertThat(parsedProblem.getResourceErrors(), empty()); |
215 | var problem = parsedProblem.problem(); | 215 | var problem = parsedProblem.problem(); |
216 | 216 | ||
217 | var storeBuilder = ModelStore.builder() | 217 | var storeBuilder = ModelStore.builder() |
@@ -278,7 +278,7 @@ class ModelGenerationTest { | |||
278 | 278 | ||
279 | scope Filesystem += 0, Entry = 100. | 279 | scope Filesystem += 0, Entry = 100. |
280 | """); | 280 | """); |
281 | assertThat(parsedProblem.errors(), empty()); | 281 | assertThat(parsedProblem.getResourceErrors(), empty()); |
282 | var problem = parsedProblem.problem(); | 282 | var problem = parsedProblem.problem(); |
283 | 283 | ||
284 | var storeBuilder = ModelStore.builder() | 284 | var storeBuilder = ModelStore.builder() |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext index 0a91178b..0fb96954 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext +++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext | |||
@@ -40,10 +40,13 @@ enum ReferenceKind: | |||
40 | ReferenceDeclaration: | 40 | ReferenceDeclaration: |
41 | (referenceType=[Relation|NonContainmentQualifiedName] | | 41 | (referenceType=[Relation|NonContainmentQualifiedName] | |
42 | kind=ReferenceKind referenceType=[Relation|QualifiedName]) | 42 | kind=ReferenceKind referenceType=[Relation|QualifiedName]) |
43 | ("[" multiplicity=Multiplicity "]")? | 43 | (multiplicity=ReferenceMultiplicity)? |
44 | name=Identifier | 44 | name=Identifier |
45 | ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; | 45 | ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; |
46 | 46 | ||
47 | ReferenceMultiplicity returns Multiplicity: | ||
48 | "[" Multiplicity "]"; | ||
49 | |||
47 | //enum PrimitiveType: | 50 | //enum PrimitiveType: |
48 | // INT="int" | REAL="real" | STRING="string"; | 51 | // INT="int" | REAL="real" | STRING="string"; |
49 | // | 52 | // |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java index 100f3781..0a5cb3c2 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java | |||
@@ -20,6 +20,7 @@ import org.eclipse.xtext.scoping.IGlobalScopeProvider; | |||
20 | import org.eclipse.xtext.scoping.IScopeProvider; | 20 | import org.eclipse.xtext.scoping.IScopeProvider; |
21 | import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; | 21 | import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; |
22 | import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; | 22 | import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; |
23 | import org.eclipse.xtext.validation.IDiagnosticConverter; | ||
23 | import org.eclipse.xtext.validation.IResourceValidator; | 24 | import org.eclipse.xtext.validation.IResourceValidator; |
24 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; | 25 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; |
25 | import tools.refinery.language.conversion.ProblemValueConverterService; | 26 | import tools.refinery.language.conversion.ProblemValueConverterService; |
@@ -33,6 +34,7 @@ import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | |||
33 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; | 34 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; |
34 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; | 35 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; |
35 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; | 36 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; |
37 | import tools.refinery.language.validation.ProblemDiagnosticConverter; | ||
36 | 38 | ||
37 | /** | 39 | /** |
38 | * Use this class to register components to be used at runtime / without the | 40 | * Use this class to register components to be used at runtime / without the |
@@ -101,4 +103,8 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
101 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { | 103 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { |
102 | return PreferShortAssertionsProblemSemanticSequencer.class; | 104 | return PreferShortAssertionsProblemSemanticSequencer.class; |
103 | } | 105 | } |
106 | |||
107 | public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { | ||
108 | return ProblemDiagnosticConverter.class; | ||
109 | } | ||
104 | } | 110 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java index b3c93b07..7b6407e1 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java | |||
@@ -7,11 +7,12 @@ package tools.refinery.language.utils; | |||
7 | 7 | ||
8 | import org.eclipse.emf.common.util.URI; | 8 | import org.eclipse.emf.common.util.URI; |
9 | import org.eclipse.emf.ecore.EObject; | 9 | import org.eclipse.emf.ecore.EObject; |
10 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
10 | import tools.refinery.language.model.problem.*; | 11 | import tools.refinery.language.model.problem.*; |
11 | 12 | ||
12 | public final class ProblemUtil { | 13 | public final class ProblemUtil { |
13 | public static final String BUILTIN_LIBRARY_NAME = "builtin"; | 14 | public static final String BUILTIN_LIBRARY_NAME = "builtin"; |
14 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME); | 15 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(); |
15 | 16 | ||
16 | private ProblemUtil() { | 17 | private ProblemUtil() { |
17 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | 18 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); |
@@ -96,8 +97,31 @@ public final class ProblemUtil { | |||
96 | throw new IllegalArgumentException("Unknown Relation: " + relation); | 97 | throw new IllegalArgumentException("Unknown Relation: " + relation); |
97 | } | 98 | } |
98 | 99 | ||
99 | private static URI getLibraryUri(String libraryName) { | 100 | public static boolean isContainerReference(ReferenceDeclaration referenceDeclaration) { |
100 | return URI.createURI(ProblemUtil.class.getClassLoader() | 101 | var kind = referenceDeclaration.getKind(); |
101 | .getResource("tools/refinery/language/%s.problem".formatted(libraryName)).toString()); | 102 | if (kind == null) { |
103 | return false; | ||
104 | } | ||
105 | return switch (kind) { | ||
106 | case CONTAINMENT -> false; | ||
107 | case CONTAINER -> true; | ||
108 | case REFERENCE -> { | ||
109 | var opposite = referenceDeclaration.getOpposite(); | ||
110 | if (opposite == null) { | ||
111 | yield false; | ||
112 | } | ||
113 | opposite = (ReferenceDeclaration) EcoreUtil.resolve(opposite, referenceDeclaration); | ||
114 | yield opposite.getKind() == ReferenceKind.CONTAINMENT; | ||
115 | } | ||
116 | }; | ||
117 | } | ||
118 | |||
119 | private static URI getLibraryUri() { | ||
120 | var libraryResource = ProblemUtil.class.getClassLoader() | ||
121 | .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME)); | ||
122 | if (libraryResource == null) { | ||
123 | throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME)); | ||
124 | } | ||
125 | return URI.createURI(libraryResource.toString()); | ||
102 | } | 126 | } |
103 | } | 127 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java new file mode 100644 index 00000000..0b7cc315 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java | |||
@@ -0,0 +1,31 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.ecore.EObject; | ||
10 | import org.eclipse.emf.ecore.EStructuralFeature; | ||
11 | import org.eclipse.xtext.validation.DiagnosticConverterImpl; | ||
12 | import tools.refinery.language.model.problem.Multiplicity; | ||
13 | import tools.refinery.language.model.problem.ReferenceDeclaration; | ||
14 | import tools.refinery.language.services.ProblemGrammarAccess; | ||
15 | |||
16 | public class ProblemDiagnosticConverter extends DiagnosticConverterImpl { | ||
17 | @Inject | ||
18 | private ProblemGrammarAccess grammarAccess; | ||
19 | |||
20 | @Override | ||
21 | protected IssueLocation getLocationData(EObject obj, EStructuralFeature structuralFeature, int index) { | ||
22 | if (structuralFeature == null && obj instanceof Multiplicity && | ||
23 | obj.eContainer() instanceof ReferenceDeclaration referenceDeclaration) { | ||
24 | // Include the enclosing {@code []} square braces in the error location. | ||
25 | // This lets use have a non-0 length error marker for invalid container references such as | ||
26 | // {@code container Foo[] foo opposite bar}, where unbounded multiplicities are disallowed. | ||
27 | return getLocationData(referenceDeclaration, obj.eContainingFeature()); | ||
28 | } | ||
29 | return super.getLocationData(obj, structuralFeature, index); | ||
30 | } | ||
31 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java index ef04726b..21b175ee 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java | |||
@@ -35,6 +35,14 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
35 | 35 | ||
36 | public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME"; | 36 | public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME"; |
37 | 37 | ||
38 | public static final String INVALID_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "INVALID_MULTIPLICITY"; | ||
39 | |||
40 | public static final String ZERO_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "ZERO_MULTIPLICITY"; | ||
41 | |||
42 | public static final String MISSING_OPPOSITE_ISSUE = ISSUE_PREFIX + "MISSING_OPPOSITE"; | ||
43 | |||
44 | public static final String INVALID_OPPOSITE_ISSUE = ISSUE_PREFIX + "INVALID_OPPOSITE"; | ||
45 | |||
38 | @Inject | 46 | @Inject |
39 | private ReferenceCounter referenceCounter; | 47 | private ReferenceCounter referenceCounter; |
40 | 48 | ||
@@ -111,4 +119,104 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
111 | } | 119 | } |
112 | } | 120 | } |
113 | } | 121 | } |
122 | |||
123 | @Check | ||
124 | public void checkRangeMultiplicity(RangeMultiplicity rangeMultiplicity) { | ||
125 | int lower = rangeMultiplicity.getLowerBound(); | ||
126 | int upper = rangeMultiplicity.getUpperBound(); | ||
127 | if (upper >= 0 && lower > upper) { | ||
128 | var message = "Multiplicity range [%d..%d] is inconsistent."; | ||
129 | acceptError(message, rangeMultiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE); | ||
130 | } | ||
131 | } | ||
132 | |||
133 | @Check | ||
134 | public void checkReferenceMultiplicity(ReferenceDeclaration referenceDeclaration) { | ||
135 | var multiplicity = referenceDeclaration.getMultiplicity(); | ||
136 | if (multiplicity == null) { | ||
137 | return; | ||
138 | } | ||
139 | if (ProblemUtil.isContainerReference(referenceDeclaration) && ( | ||
140 | !(multiplicity instanceof RangeMultiplicity rangeMultiplicity) || | ||
141 | rangeMultiplicity.getLowerBound() != 0 || | ||
142 | rangeMultiplicity.getUpperBound() != 1)) { | ||
143 | var message = "The only allowed multiplicity for container references is [0..1]"; | ||
144 | acceptError(message, multiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE); | ||
145 | } | ||
146 | if ((multiplicity instanceof ExactMultiplicity exactMultiplicity && | ||
147 | exactMultiplicity.getExactValue() == 0) || | ||
148 | (multiplicity instanceof RangeMultiplicity rangeMultiplicity && | ||
149 | rangeMultiplicity.getLowerBound() == 0 && | ||
150 | rangeMultiplicity.getUpperBound() == 0)) { | ||
151 | var message = "The multiplicity constraint does not allow any reference links"; | ||
152 | acceptWarning(message, multiplicity, null, 0, ZERO_MULTIPLICITY_ISSUE); | ||
153 | } | ||
154 | } | ||
155 | |||
156 | @Check | ||
157 | public void checkOpposite(ReferenceDeclaration referenceDeclaration) { | ||
158 | var opposite = referenceDeclaration.getOpposite(); | ||
159 | if (opposite == null || opposite.eIsProxy()) { | ||
160 | return; | ||
161 | } | ||
162 | var oppositeOfOpposite = opposite.getOpposite(); | ||
163 | if (oppositeOfOpposite == null) { | ||
164 | acceptError("Reference '%s' does not declare '%s' as an opposite." | ||
165 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
166 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
167 | INVALID_OPPOSITE_ISSUE); | ||
168 | var oppositeResource = opposite.eResource(); | ||
169 | if (oppositeResource != null && oppositeResource.equals(referenceDeclaration.eResource())) { | ||
170 | acceptError("Missing opposite '%s' for reference '%s'." | ||
171 | .formatted(referenceDeclaration.getName(), opposite.getName()), | ||
172 | opposite, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE); | ||
173 | } | ||
174 | return; | ||
175 | } | ||
176 | if (!referenceDeclaration.equals(oppositeOfOpposite)) { | ||
177 | var messageBuilder = new StringBuilder() | ||
178 | .append("Expected reference '") | ||
179 | .append(opposite.getName()) | ||
180 | .append("' to have opposite '") | ||
181 | .append(referenceDeclaration.getName()) | ||
182 | .append("'"); | ||
183 | var oppositeOfOppositeName = oppositeOfOpposite.getName(); | ||
184 | if (oppositeOfOppositeName != null) { | ||
185 | messageBuilder.append(", got '") | ||
186 | .append(oppositeOfOppositeName) | ||
187 | .append("' instead"); | ||
188 | } | ||
189 | messageBuilder.append("."); | ||
190 | acceptError(messageBuilder.toString(), referenceDeclaration, | ||
191 | ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, INVALID_OPPOSITE_ISSUE); | ||
192 | } | ||
193 | } | ||
194 | |||
195 | @Check | ||
196 | void checkContainerOpposite(ReferenceDeclaration referenceDeclaration) { | ||
197 | var kind = referenceDeclaration.getKind(); | ||
198 | var opposite = referenceDeclaration.getOpposite(); | ||
199 | if (opposite != null && opposite.eIsProxy()) { | ||
200 | // If {@code opposite} is a proxy, we have already emitted a linker error. | ||
201 | return; | ||
202 | } | ||
203 | if (kind == ReferenceKind.CONTAINMENT) { | ||
204 | if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) { | ||
205 | acceptError("Opposite '%s' of containment reference '%s' is not a container reference." | ||
206 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
207 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
208 | INVALID_OPPOSITE_ISSUE); | ||
209 | } | ||
210 | } else if (kind == ReferenceKind.CONTAINER) { | ||
211 | if (opposite == null) { | ||
212 | acceptError("Container reference '%s' requires an opposite.".formatted(referenceDeclaration.getName()), | ||
213 | referenceDeclaration, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE); | ||
214 | } else if (opposite.getKind() != ReferenceKind.CONTAINMENT) { | ||
215 | acceptError("Opposite '%s' of container reference '%s' is not a containment reference." | ||
216 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
217 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
218 | INVALID_OPPOSITE_ISSUE); | ||
219 | } | ||
220 | } | ||
221 | } | ||
114 | } | 222 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java index 96e7cf9c..72d57f54 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java | |||
@@ -51,6 +51,6 @@ class ProblemParsingTest { | |||
51 | ?children(bob, ciri). | 51 | ?children(bob, ciri). |
52 | taxStatus(anne, ADULT). | 52 | taxStatus(anne, ADULT). |
53 | """); | 53 | """); |
54 | assertThat(problem.errors(), empty()); | 54 | assertThat(problem.getResourceErrors(), empty()); |
55 | } | 55 | } |
56 | } | 56 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java index b1b24ef3..464c207c 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java | |||
@@ -7,6 +7,7 @@ package tools.refinery.language.tests.linking; | |||
7 | 7 | ||
8 | 8 | ||
9 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
10 | import org.eclipse.xtext.diagnostics.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | 11 | import org.eclipse.xtext.testing.InjectWith; |
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | 12 | import org.eclipse.xtext.testing.extensions.InjectionExtension; |
12 | import org.junit.jupiter.api.extension.ExtendWith; | 13 | import org.junit.jupiter.api.extension.ExtendWith; |
@@ -57,7 +58,7 @@ class AmbiguousReferenceTest { | |||
57 | """}) | 58 | """}) |
58 | void unambiguousReferenceTest(String text) { | 59 | void unambiguousReferenceTest(String text) { |
59 | var problem = parseHelper.parse(text); | 60 | var problem = parseHelper.parse(text); |
60 | assertThat(problem.errors(), empty()); | 61 | assertThat(problem.getResourceErrors(), empty()); |
61 | } | 62 | } |
62 | 63 | ||
63 | @ParameterizedTest | 64 | @ParameterizedTest |
@@ -88,7 +89,10 @@ class AmbiguousReferenceTest { | |||
88 | """}) | 89 | """}) |
89 | void ambiguousReferenceTest(String text) { | 90 | void ambiguousReferenceTest(String text) { |
90 | var problem = parseHelper.parse(text); | 91 | var problem = parseHelper.parse(text); |
91 | assertThat(problem.errors(), hasItem(hasProperty("message", stringContainsInOrder( | 92 | var errors = problem.getResourceErrors(); |
92 | "Ambiguous reference", "'quux'")))); | 93 | assertThat(problem.getResourceErrors(), hasItem(allOf( |
94 | hasProperty("code", is(Diagnostic.LINKING_DIAGNOSTIC)), | ||
95 | hasProperty("message", containsString("'quux'")) | ||
96 | ))); | ||
93 | } | 97 | } |
94 | } | 98 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java index ed193e90..a9c5f62a 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java | |||
@@ -31,7 +31,7 @@ class TransitiveClosureParserTest { | |||
31 | var problem = parseHelper.parse(""" | 31 | var problem = parseHelper.parse(""" |
32 | pred foo(a, b) <-> a + (b) > 10. | 32 | pred foo(a, b) <-> a + (b) > 10. |
33 | """); | 33 | """); |
34 | assertThat(problem.errors(), empty()); | 34 | assertThat(problem.getResourceErrors(), empty()); |
35 | var literal = problem.pred("foo").conj(0).lit(0).get(); | 35 | var literal = problem.pred("foo").conj(0).lit(0).get(); |
36 | assertThat(literal, instanceOf(ComparisonExpr.class)); | 36 | assertThat(literal, instanceOf(ComparisonExpr.class)); |
37 | var left = ((ComparisonExpr) literal).getLeft(); | 37 | var left = ((ComparisonExpr) literal).getLeft(); |
@@ -45,7 +45,7 @@ class TransitiveClosureParserTest { | |||
45 | var problem = parseHelper.parse(""" | 45 | var problem = parseHelper.parse(""" |
46 | pred foo(a, b) <-> equals+(a, b). | 46 | pred foo(a, b) <-> equals+(a, b). |
47 | """); | 47 | """); |
48 | assertThat(problem.errors(), empty()); | 48 | assertThat(problem.getResourceErrors(), empty()); |
49 | var literal = problem.pred("foo").conj(0).lit(0).get(); | 49 | var literal = problem.pred("foo").conj(0).lit(0).get(); |
50 | assertThat(literal, instanceOf(Atom.class)); | 50 | assertThat(literal, instanceOf(Atom.class)); |
51 | var atom = (Atom) literal; | 51 | var atom = (Atom) literal; |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java index 68514bfa..56e65550 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java | |||
@@ -42,7 +42,7 @@ class RuleParsingTest { | |||
42 | """ }) | 42 | """ }) |
43 | void simpleTest(String text) { | 43 | void simpleTest(String text) { |
44 | var problem = parseHelper.parse(text); | 44 | var problem = parseHelper.parse(text); |
45 | assertThat(problem.errors(), empty()); | 45 | assertThat(problem.getResourceErrors(), empty()); |
46 | } | 46 | } |
47 | 47 | ||
48 | @Test | 48 | @Test |
@@ -51,7 +51,7 @@ class RuleParsingTest { | |||
51 | pred Person(p). | 51 | pred Person(p). |
52 | rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. | 52 | rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. |
53 | """); | 53 | """); |
54 | assertThat(problem.errors(), empty()); | 54 | assertThat(problem.getResourceErrors(), empty()); |
55 | assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); | 55 | assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); |
56 | assertThat(problem.rule("r").consequent(0).action(0).newVar(), | 56 | assertThat(problem.rule("r").consequent(0).action(0).newVar(), |
57 | equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); | 57 | equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); |
@@ -63,7 +63,7 @@ class RuleParsingTest { | |||
63 | pred Friend(a, b). | 63 | pred Friend(a, b). |
64 | rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. | 64 | rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. |
65 | """); | 65 | """); |
66 | assertThat(problem.errors(), empty()); | 66 | assertThat(problem.getResourceErrors(), empty()); |
67 | assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), | 67 | assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), |
68 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); | 68 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); |
69 | } | 69 | } |
@@ -74,7 +74,7 @@ class RuleParsingTest { | |||
74 | pred Friend(a, b). | 74 | pred Friend(a, b). |
75 | rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. | 75 | rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. |
76 | """); | 76 | """); |
77 | assertThat(problem.errors(), empty()); | 77 | assertThat(problem.getResourceErrors(), empty()); |
78 | assertThat(problem.rule("r").param(1), | 78 | assertThat(problem.rule("r").param(1), |
79 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); | 79 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); |
80 | } | 80 | } |
@@ -85,6 +85,6 @@ class RuleParsingTest { | |||
85 | pred Person(p). | 85 | pred Person(p). |
86 | rule r(p1): must Friend(p1, p2) ==> delete p2. | 86 | rule r(p1): must Friend(p1, p2) ==> delete p2. |
87 | """); | 87 | """); |
88 | assertThat(problem.errors(), not(empty())); | 88 | assertThat(problem.getResourceErrors(), not(empty())); |
89 | } | 89 | } |
90 | } | 90 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java index 734bfcd1..e76d2993 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java | |||
@@ -36,7 +36,7 @@ class NodeScopingTest { | |||
36 | var problem = parse(""" | 36 | var problem = parse(""" |
37 | pred predicate({PARAM}node a). | 37 | pred predicate({PARAM}node a). |
38 | """, qualifiedNamePrefix); | 38 | """, qualifiedNamePrefix); |
39 | assertThat(problem.errors(), empty()); | 39 | assertThat(problem.getResourceErrors(), empty()); |
40 | assertThat(problem.pred("predicate").param(0).getParameterType(), | 40 | assertThat(problem.pred("predicate").param(0).getParameterType(), |
41 | equalTo(problem.builtin().findClass("node").get())); | 41 | equalTo(problem.builtin().findClass("node").get())); |
42 | } | 42 | } |
@@ -48,7 +48,7 @@ class NodeScopingTest { | |||
48 | predicate(a, a). | 48 | predicate(a, a). |
49 | ?predicate(a, b). | 49 | ?predicate(a, b). |
50 | """); | 50 | """); |
51 | assertThat(problem.errors(), empty()); | 51 | assertThat(problem.getResourceErrors(), empty()); |
52 | assertThat(problem.nodeNames(), hasItems("a", "b")); | 52 | assertThat(problem.nodeNames(), hasItems("a", "b")); |
53 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); | 53 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); |
54 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); | 54 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); |
@@ -62,7 +62,7 @@ class NodeScopingTest { | |||
62 | pred predicate(node a) <-> node(b). | 62 | pred predicate(node a) <-> node(b). |
63 | predicate(b). | 63 | predicate(b). |
64 | """); | 64 | """); |
65 | assertThat(problem.errors(), empty()); | 65 | assertThat(problem.getResourceErrors(), empty()); |
66 | assertThat(problem.nodeNames(), hasItems("b")); | 66 | assertThat(problem.nodeNames(), hasItems("b")); |
67 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); | 67 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); |
68 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); | 68 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); |
@@ -77,7 +77,7 @@ class NodeScopingTest { | |||
77 | predicate({PARAM}a, {PARAM}a). | 77 | predicate({PARAM}a, {PARAM}a). |
78 | ?predicate({PARAM}a, {PARAM}b). | 78 | ?predicate({PARAM}a, {PARAM}b). |
79 | """, qualifiedNamePrefix, namedProblem); | 79 | """, qualifiedNamePrefix, namedProblem); |
80 | assertThat(problem.errors(), empty()); | 80 | assertThat(problem.getResourceErrors(), empty()); |
81 | assertThat(problem.nodeNames(), empty()); | 81 | assertThat(problem.nodeNames(), empty()); |
82 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); | 82 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); |
83 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); | 83 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); |
@@ -92,7 +92,7 @@ class NodeScopingTest { | |||
92 | indiv b. | 92 | indiv b. |
93 | pred predicate(node a) <-> node({PARAM}b). | 93 | pred predicate(node a) <-> node({PARAM}b). |
94 | """); | 94 | """); |
95 | assertThat(problem.errors(), empty()); | 95 | assertThat(problem.getResourceErrors(), empty()); |
96 | assertThat(problem.nodeNames(), empty()); | 96 | assertThat(problem.nodeNames(), empty()); |
97 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); | 97 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); |
98 | } | 98 | } |
@@ -109,7 +109,7 @@ class NodeScopingTest { | |||
109 | pred predicate(node x) <-> node(x). | 109 | pred predicate(node x) <-> node(x). |
110 | predicate({PARAM}). | 110 | predicate({PARAM}). |
111 | """, qualifiedName); | 111 | """, qualifiedName); |
112 | assertThat(problem.errors(), empty()); | 112 | assertThat(problem.getResourceErrors(), empty()); |
113 | assertThat(problem.nodeNames(), empty()); | 113 | assertThat(problem.nodeNames(), empty()); |
114 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); | 114 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); |
115 | } | 115 | } |
@@ -121,7 +121,7 @@ class NodeScopingTest { | |||
121 | var problem = parse(""" | 121 | var problem = parse(""" |
122 | pred predicate(node x) <-> node({PARAM}). | 122 | pred predicate(node x) <-> node({PARAM}). |
123 | """, qualifiedName); | 123 | """, qualifiedName); |
124 | assertThat(problem.errors(), empty()); | 124 | assertThat(problem.getResourceErrors(), empty()); |
125 | assertThat(problem.nodeNames(), empty()); | 125 | assertThat(problem.nodeNames(), empty()); |
126 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 126 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
127 | equalTo(problem.builtin().findClass("int").get().getNewNode())); | 127 | equalTo(problem.builtin().findClass("int").get().getNewNode())); |
@@ -139,7 +139,7 @@ class NodeScopingTest { | |||
139 | pred predicate(node x) <-> node(x). | 139 | pred predicate(node x) <-> node(x). |
140 | predicate({PARAM}). | 140 | predicate({PARAM}). |
141 | """, qualifiedName, namedProblem); | 141 | """, qualifiedName, namedProblem); |
142 | assertThat(problem.errors(), empty()); | 142 | assertThat(problem.getResourceErrors(), empty()); |
143 | assertThat(problem.nodeNames(), empty()); | 143 | assertThat(problem.nodeNames(), empty()); |
144 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); | 144 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); |
145 | } | 145 | } |
@@ -151,7 +151,7 @@ class NodeScopingTest { | |||
151 | class Foo. | 151 | class Foo. |
152 | pred predicate(node x) <-> node({PARAM}). | 152 | pred predicate(node x) <-> node({PARAM}). |
153 | """, qualifiedName, namedProblem); | 153 | """, qualifiedName, namedProblem); |
154 | assertThat(problem.errors(), empty()); | 154 | assertThat(problem.getResourceErrors(), empty()); |
155 | assertThat(problem.nodeNames(), empty()); | 155 | assertThat(problem.nodeNames(), empty()); |
156 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 156 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
157 | equalTo(problem.findClass("Foo").get().getNewNode())); | 157 | equalTo(problem.findClass("Foo").get().getNewNode())); |
@@ -169,7 +169,7 @@ class NodeScopingTest { | |||
169 | pred predicate(node x) <-> node(x). | 169 | pred predicate(node x) <-> node(x). |
170 | predicate(new). | 170 | predicate(new). |
171 | """); | 171 | """); |
172 | assertThat(problem.errors(), empty()); | 172 | assertThat(problem.getResourceErrors(), empty()); |
173 | assertThat(problem.nodeNames(), hasItems("new")); | 173 | assertThat(problem.nodeNames(), hasItems("new")); |
174 | assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); | 174 | assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); |
175 | } | 175 | } |
@@ -182,7 +182,7 @@ class NodeScopingTest { | |||
182 | pred predicate(Foo a) <-> node(a). | 182 | pred predicate(Foo a) <-> node(a). |
183 | predicate({PARAM}). | 183 | predicate({PARAM}). |
184 | """, qualifiedName, namedProblem); | 184 | """, qualifiedName, namedProblem); |
185 | assertThat(problem.errors(), empty()); | 185 | assertThat(problem.getResourceErrors(), empty()); |
186 | assertThat(problem.nodeNames(), empty()); | 186 | assertThat(problem.nodeNames(), empty()); |
187 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); | 187 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); |
188 | } | 188 | } |
@@ -194,7 +194,7 @@ class NodeScopingTest { | |||
194 | enum Foo { alpha, beta } | 194 | enum Foo { alpha, beta } |
195 | pred predicate(Foo a) <-> node({PARAM}). | 195 | pred predicate(Foo a) <-> node({PARAM}). |
196 | """, qualifiedName, namedProblem); | 196 | """, qualifiedName, namedProblem); |
197 | assertThat(problem.errors(), empty()); | 197 | assertThat(problem.getResourceErrors(), empty()); |
198 | assertThat(problem.nodeNames(), empty()); | 198 | assertThat(problem.nodeNames(), empty()); |
199 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 199 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
200 | equalTo(problem.findEnum("Foo").literal("alpha"))); | 200 | equalTo(problem.findEnum("Foo").literal("alpha"))); |
@@ -214,7 +214,7 @@ class NodeScopingTest { | |||
214 | pred predicate(node a) <-> node(a). | 214 | pred predicate(node a) <-> node(a). |
215 | predicate({PARAM}). | 215 | predicate({PARAM}). |
216 | """, qualifiedName); | 216 | """, qualifiedName); |
217 | assertThat(problem.errors(), empty()); | 217 | assertThat(problem.getResourceErrors(), empty()); |
218 | assertThat(problem.nodeNames(), empty()); | 218 | assertThat(problem.nodeNames(), empty()); |
219 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); | 219 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); |
220 | } | 220 | } |
@@ -226,7 +226,7 @@ class NodeScopingTest { | |||
226 | var problem = parse(""" | 226 | var problem = parse(""" |
227 | pred predicate() <-> node({PARAM}). | 227 | pred predicate() <-> node({PARAM}). |
228 | """, qualifiedName); | 228 | """, qualifiedName); |
229 | assertThat(problem.errors(), empty()); | 229 | assertThat(problem.getResourceErrors(), empty()); |
230 | assertThat(problem.nodeNames(), empty()); | 230 | assertThat(problem.nodeNames(), empty()); |
231 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 231 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
232 | equalTo(problem.builtin().findEnum("bool").literal("true"))); | 232 | equalTo(problem.builtin().findEnum("bool").literal("true"))); |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java new file mode 100644 index 00000000..a8bcb1a6 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java | |||
@@ -0,0 +1,119 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.extension.ExtendWith; | ||
13 | import org.junit.jupiter.params.ParameterizedTest; | ||
14 | import org.junit.jupiter.params.provider.ValueSource; | ||
15 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
16 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
17 | import tools.refinery.language.validation.ProblemValidator; | ||
18 | |||
19 | import static org.hamcrest.MatcherAssert.assertThat; | ||
20 | import static org.hamcrest.Matchers.*; | ||
21 | |||
22 | @ExtendWith(InjectionExtension.class) | ||
23 | @InjectWith(ProblemInjectorProvider.class) | ||
24 | class MultiplicityValidationTest { | ||
25 | @Inject | ||
26 | private ProblemParseHelper parseHelper; | ||
27 | |||
28 | @ParameterizedTest | ||
29 | @ValueSource(strings = {"2..5", "2..2", "2..*", "2", ""}) | ||
30 | void validReferenceMultiplicityTest(String range) { | ||
31 | var problem = parseHelper.parse(""" | ||
32 | class Foo { | ||
33 | Bar[%s] bar | ||
34 | } | ||
35 | |||
36 | class Bar. | ||
37 | """.formatted(range)); | ||
38 | assertThat(problem.validate(), empty()); | ||
39 | } | ||
40 | |||
41 | @ParameterizedTest | ||
42 | @ValueSource(strings = {"2..5", "2..2", "2..*", "2", "0..0", "0"}) | ||
43 | void validScopeMultiplicityTest(String range) { | ||
44 | var problem = parseHelper.parse(""" | ||
45 | class Foo. | ||
46 | |||
47 | scope Foo = %s. | ||
48 | """.formatted(range)); | ||
49 | assertThat(problem.validate(), empty()); | ||
50 | } | ||
51 | |||
52 | |||
53 | @ParameterizedTest | ||
54 | @ValueSource(strings = {"0", "0..0"}) | ||
55 | void zeroMReferenceMultiplicityTest(String range) { | ||
56 | var problem = parseHelper.parse(""" | ||
57 | class Foo { | ||
58 | Bar[%s] bar | ||
59 | } | ||
60 | |||
61 | class Bar. | ||
62 | """.formatted(range)); | ||
63 | assertThat(problem.validate(), hasItem(allOf( | ||
64 | hasProperty("severity", is(Diagnostic.WARNING)), | ||
65 | hasProperty("issueCode", is(ProblemValidator.ZERO_MULTIPLICITY_ISSUE)) | ||
66 | ))); | ||
67 | } | ||
68 | |||
69 | @ParameterizedTest | ||
70 | @ValueSource(strings = { | ||
71 | "container Bar bar opposite foo", | ||
72 | "container Bar[0..1] bar opposite foo", | ||
73 | "container Bar bar", // Invalid, but has valid multiplicity. | ||
74 | "container Bar[0..1] bar", // Invalid, but has valid multiplicity. | ||
75 | "Bar bar opposite foo", | ||
76 | "Bar[0..1] bar opposite foo" | ||
77 | }) | ||
78 | void validContainerReference(String referenceText) { | ||
79 | var problem = parseHelper.parse(""" | ||
80 | class Foo { | ||
81 | %s | ||
82 | } | ||
83 | |||
84 | class Bar { | ||
85 | contains Foo foo opposite bar | ||
86 | } | ||
87 | """.formatted(referenceText)); | ||
88 | assertThat(problem.validate(), not(hasItem(hasProperty("issueCode", | ||
89 | is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE))))); | ||
90 | } | ||
91 | |||
92 | @ParameterizedTest | ||
93 | @ValueSource(strings = { | ||
94 | "container Bar[1] bar opposite foo", | ||
95 | "container Bar[1..2] bar opposite foo", | ||
96 | "container Bar[] bar opposite foo", | ||
97 | "container Bar[1] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
98 | "container Bar[1..2] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
99 | "container Bar[] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
100 | "Bar[1] bar opposite foo", | ||
101 | "Bar[1..2] bar opposite foo", | ||
102 | "Bar[] bar opposite foo" | ||
103 | }) | ||
104 | void invalidContainerReference(String referenceText) { | ||
105 | var problem = parseHelper.parse(""" | ||
106 | class Foo { | ||
107 | %s | ||
108 | } | ||
109 | |||
110 | class Bar { | ||
111 | contains Foo foo opposite bar | ||
112 | } | ||
113 | """.formatted(referenceText)); | ||
114 | assertThat(problem.validate(), hasItem(allOf( | ||
115 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
116 | hasProperty("issueCode", is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE)) | ||
117 | ))); | ||
118 | } | ||
119 | } | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java new file mode 100644 index 00000000..57602377 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java | |||
@@ -0,0 +1,209 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.Test; | ||
13 | import org.junit.jupiter.api.extension.ExtendWith; | ||
14 | import org.junit.jupiter.params.ParameterizedTest; | ||
15 | import org.junit.jupiter.params.provider.ValueSource; | ||
16 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
17 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
18 | import tools.refinery.language.validation.ProblemValidator; | ||
19 | |||
20 | import java.util.Set; | ||
21 | |||
22 | import static org.hamcrest.MatcherAssert.assertThat; | ||
23 | import static org.hamcrest.Matchers.*; | ||
24 | |||
25 | @ExtendWith(InjectionExtension.class) | ||
26 | @InjectWith(ProblemInjectorProvider.class) | ||
27 | class OppositeValidationTest { | ||
28 | @Inject | ||
29 | private ProblemParseHelper parseHelper; | ||
30 | |||
31 | @ParameterizedTest | ||
32 | @ValueSource(strings = {""" | ||
33 | class Foo { | ||
34 | Bar bar opposite foo | ||
35 | } | ||
36 | |||
37 | class Bar { | ||
38 | Foo foo opposite bar | ||
39 | } | ||
40 | """, """ | ||
41 | class Foo { | ||
42 | contains Bar bar opposite foo | ||
43 | } | ||
44 | |||
45 | class Bar { | ||
46 | Foo foo opposite bar | ||
47 | } | ||
48 | """, """ | ||
49 | class Foo { | ||
50 | contains Bar bar opposite foo | ||
51 | } | ||
52 | |||
53 | class Bar { | ||
54 | container Foo foo opposite bar | ||
55 | } | ||
56 | """, """ | ||
57 | class Foo { | ||
58 | Foo foo[] opposite foo | ||
59 | } | ||
60 | """}) | ||
61 | void validOppositeTest(String text) { | ||
62 | var problem = parseHelper.parse(text); | ||
63 | var issues = problem.validate(); | ||
64 | assertThat(issues, not(hasItems(hasProperty("issueCode", in(Set.of( | ||
65 | ProblemValidator.INVALID_OPPOSITE_ISSUE, | ||
66 | ProblemValidator.MISSING_OPPOSITE_ISSUE | ||
67 | )))))); | ||
68 | } | ||
69 | |||
70 | @Test | ||
71 | void missingOppositeTest() { | ||
72 | var problem = parseHelper.parse(""" | ||
73 | class Foo { | ||
74 | Bar bar opposite foo | ||
75 | } | ||
76 | |||
77 | class Bar { | ||
78 | Foo foo | ||
79 | } | ||
80 | """); | ||
81 | var issues = problem.validate(); | ||
82 | assertThat(issues, hasItems(allOf( | ||
83 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
84 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
85 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
86 | ), allOf( | ||
87 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
88 | hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)), | ||
89 | hasProperty("message", stringContainsInOrder("bar", "foo")) | ||
90 | ))); | ||
91 | } | ||
92 | |||
93 | @Test | ||
94 | void oppositeMismatchTest() { | ||
95 | var problem = parseHelper.parse(""" | ||
96 | class Foo { | ||
97 | Bar bar opposite foo | ||
98 | Bar quux opposite foo | ||
99 | } | ||
100 | |||
101 | class Bar { | ||
102 | Foo foo opposite bar | ||
103 | } | ||
104 | """); | ||
105 | var issues = problem.validate(); | ||
106 | assertThat(issues, hasItem(allOf( | ||
107 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
108 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
109 | hasProperty("message", stringContainsInOrder("foo", "quux", "bar")) | ||
110 | ))); | ||
111 | } | ||
112 | |||
113 | @Test | ||
114 | void oppositeMismatchProxyTest() { | ||
115 | var problem = parseHelper.parse(""" | ||
116 | class Foo { | ||
117 | Bar bar opposite foo | ||
118 | } | ||
119 | |||
120 | class Bar { | ||
121 | Foo foo opposite quux | ||
122 | } | ||
123 | """); | ||
124 | var issues = problem.validate(); | ||
125 | assertThat(issues, hasItem(allOf( | ||
126 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
127 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
128 | hasProperty("message", allOf( | ||
129 | stringContainsInOrder("foo", "bar"), | ||
130 | not(containsString("null")) | ||
131 | )) | ||
132 | ))); | ||
133 | } | ||
134 | |||
135 | @ParameterizedTest | ||
136 | @ValueSource(strings = {"contains", "container"}) | ||
137 | void containmentWithProxyOppositeTest(String keyword) { | ||
138 | var problem = parseHelper.parse(""" | ||
139 | class Foo { | ||
140 | %s Bar bar opposite foo | ||
141 | } | ||
142 | |||
143 | class Bar. | ||
144 | """.formatted(keyword)); | ||
145 | var issues = problem.validate(); | ||
146 | assertThat(issues, not(hasItem(hasProperty("issueCode", | ||
147 | is(ProblemValidator.INVALID_OPPOSITE_ISSUE))))); | ||
148 | } | ||
149 | |||
150 | @Test | ||
151 | void containmentWithContainmentOppositeTest() { | ||
152 | var problem = parseHelper.parse(""" | ||
153 | class Foo { | ||
154 | contains Bar bar opposite foo | ||
155 | } | ||
156 | |||
157 | class Bar { | ||
158 | contains Foo foo opposite bar | ||
159 | } | ||
160 | """); | ||
161 | var issues = problem.validate(); | ||
162 | assertThat(issues, hasItems(allOf( | ||
163 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
164 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
165 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
166 | ), allOf( | ||
167 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
168 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
169 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
170 | ))); | ||
171 | } | ||
172 | |||
173 | @Test | ||
174 | void containerWithoutOppositeTest() { | ||
175 | var problem = parseHelper.parse(""" | ||
176 | class Foo { | ||
177 | container Bar bar | ||
178 | } | ||
179 | |||
180 | class Bar. | ||
181 | """); | ||
182 | var issues = problem.validate(); | ||
183 | assertThat(issues, hasItem(allOf( | ||
184 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
185 | hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)), | ||
186 | hasProperty("message", containsString("bar")) | ||
187 | ))); | ||
188 | } | ||
189 | |||
190 | @ParameterizedTest | ||
191 | @ValueSource(strings = {"Foo foo", "container Foo foo"}) | ||
192 | void containerInvalidOppositeTest(String reference) { | ||
193 | var problem = parseHelper.parse(""" | ||
194 | class Foo { | ||
195 | container Bar bar opposite foo | ||
196 | } | ||
197 | |||
198 | class Bar { | ||
199 | %s opposite bar | ||
200 | } | ||
201 | """.formatted(reference)); | ||
202 | var issues = problem.validate(); | ||
203 | assertThat(issues, hasItem(allOf( | ||
204 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
205 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
206 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
207 | ))); | ||
208 | } | ||
209 | } | ||
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java index 6f6a87f7..f1535716 100644 --- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java +++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java | |||
@@ -5,23 +5,26 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.model.tests.utils; | 6 | package tools.refinery.language.model.tests.utils; |
7 | 7 | ||
8 | import org.eclipse.xtext.testing.util.ParseHelper; | ||
9 | |||
10 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
11 | 9 | import org.eclipse.emf.ecore.util.EcoreUtil; | |
10 | import org.eclipse.xtext.testing.util.ParseHelper; | ||
11 | import org.eclipse.xtext.validation.IResourceValidator; | ||
12 | import tools.refinery.language.model.problem.Problem; | 12 | import tools.refinery.language.model.problem.Problem; |
13 | 13 | ||
14 | public class ProblemParseHelper { | 14 | public class ProblemParseHelper { |
15 | @Inject | 15 | @Inject |
16 | private IResourceValidator resourceValidator; | ||
17 | @Inject | ||
16 | private ParseHelper<Problem> parseHelper; | 18 | private ParseHelper<Problem> parseHelper; |
17 | 19 | ||
18 | public WrappedProblem parse(String text) { | 20 | public WrappedProblem parse(String text) { |
19 | Problem problem; | 21 | Problem problem; |
20 | try { | 22 | try { |
21 | problem = parseHelper.parse(text); | 23 | problem = parseHelper.parse(text); |
22 | } catch (Exception e) { | 24 | } catch (Exception e) { |
23 | throw new RuntimeException("Unexpected exception while parsing Problem", e); | 25 | throw new AssertionError("Unexpected exception while parsing Problem", e); |
24 | } | 26 | } |
27 | EcoreUtil.resolveAll(problem); | ||
25 | return new WrappedProblem(problem); | 28 | return new WrappedProblem(problem); |
26 | } | 29 | } |
27 | } | 30 | } |
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java index e5aa0043..fc51ff57 100644 --- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java +++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java | |||
@@ -6,7 +6,7 @@ | |||
6 | package tools.refinery.language.model.tests.utils; | 6 | package tools.refinery.language.model.tests.utils; |
7 | 7 | ||
8 | import org.eclipse.emf.ecore.resource.Resource.Diagnostic; | 8 | import org.eclipse.emf.ecore.resource.Resource.Diagnostic; |
9 | import org.eclipse.emf.ecore.util.EcoreUtil; | 9 | import org.eclipse.emf.ecore.util.Diagnostician; |
10 | import tools.refinery.language.model.problem.*; | 10 | import tools.refinery.language.model.problem.*; |
11 | import tools.refinery.language.utils.BuiltinSymbols; | 11 | import tools.refinery.language.utils.BuiltinSymbols; |
12 | import tools.refinery.language.utils.ProblemDesugarer; | 12 | import tools.refinery.language.utils.ProblemDesugarer; |
@@ -19,11 +19,18 @@ public record WrappedProblem(Problem problem) { | |||
19 | return problem; | 19 | return problem; |
20 | } | 20 | } |
21 | 21 | ||
22 | public List<Diagnostic> errors() { | 22 | public List<Diagnostic> getResourceErrors() { |
23 | EcoreUtil.resolveAll(problem); | ||
24 | return problem.eResource().getErrors(); | 23 | return problem.eResource().getErrors(); |
25 | } | 24 | } |
26 | 25 | ||
26 | public List<Diagnostic> getResourceWarnings() { | ||
27 | return problem.eResource().getWarnings(); | ||
28 | } | ||
29 | |||
30 | public List<org.eclipse.emf.common.util.Diagnostic> validate() { | ||
31 | return Diagnostician.INSTANCE.validate(problem).getChildren(); | ||
32 | } | ||
33 | |||
27 | public WrappedProblem builtin() { | 34 | public WrappedProblem builtin() { |
28 | return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); | 35 | return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); |
29 | } | 36 | } |