diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-11-05 20:43:55 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-11-17 12:41:33 +0100 |
commit | 31baf36ddbdd3f34318b7bd22c6e773fbdc846ad (patch) | |
tree | c35e7517abee69c307e6f35908a85a6fc80ebf3e | |
parent | build: prepare for Maven publication (diff) | |
download | refinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.tar.gz refinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.tar.zst refinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.zip |
feat(langugage): detect ambiguous references
5 files changed, 439 insertions, 2 deletions
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 2636a8ee..100f3781 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java | |||
@@ -12,6 +12,7 @@ package tools.refinery.language; | |||
12 | import com.google.inject.Binder; | 12 | import com.google.inject.Binder; |
13 | import com.google.inject.name.Names; | 13 | import com.google.inject.name.Names; |
14 | import org.eclipse.xtext.conversion.IValueConverterService; | 14 | import org.eclipse.xtext.conversion.IValueConverterService; |
15 | import org.eclipse.xtext.linking.ILinkingService; | ||
15 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
16 | import org.eclipse.xtext.parser.IParser; | 17 | import org.eclipse.xtext.parser.IParser; |
17 | import org.eclipse.xtext.resource.*; | 18 | import org.eclipse.xtext.resource.*; |
@@ -22,10 +23,12 @@ import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; | |||
22 | import org.eclipse.xtext.validation.IResourceValidator; | 23 | import org.eclipse.xtext.validation.IResourceValidator; |
23 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; | 24 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; |
24 | import tools.refinery.language.conversion.ProblemValueConverterService; | 25 | import tools.refinery.language.conversion.ProblemValueConverterService; |
26 | import tools.refinery.language.linking.ProblemLinkingService; | ||
25 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; | 27 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; |
26 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; | 28 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; |
27 | import tools.refinery.language.resource.ProblemDerivedStateComputer; | 29 | import tools.refinery.language.resource.ProblemDerivedStateComputer; |
28 | import tools.refinery.language.resource.ProblemLocationInFileProvider; | 30 | import tools.refinery.language.resource.ProblemLocationInFileProvider; |
31 | import tools.refinery.language.resource.ProblemResource; | ||
29 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | 32 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; |
30 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; | 33 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; |
31 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; | 34 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; |
@@ -55,6 +58,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
55 | } | 58 | } |
56 | 59 | ||
57 | @Override | 60 | @Override |
61 | public Class<? extends ILinkingService> bindILinkingService() { | ||
62 | return ProblemLinkingService.class; | ||
63 | } | ||
64 | |||
65 | @Override | ||
58 | public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { | 66 | public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { |
59 | return ProblemGlobalScopeProvider.class; | 67 | return ProblemGlobalScopeProvider.class; |
60 | } | 68 | } |
@@ -67,7 +75,7 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
67 | 75 | ||
68 | @Override | 76 | @Override |
69 | public Class<? extends XtextResource> bindXtextResource() { | 77 | public Class<? extends XtextResource> bindXtextResource() { |
70 | return DerivedStateAwareResource.class; | 78 | return ProblemResource.class; |
71 | } | 79 | } |
72 | 80 | ||
73 | // Method name follows Xtext convention. | 81 | // Method name follows Xtext convention. |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java new file mode 100644 index 00000000..511ed420 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java | |||
@@ -0,0 +1,72 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2008, 2018 itemis AG (http://www.itemis.eu) and others. | ||
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
4 | * This program and the accompanying materials are made available under the | ||
5 | * terms of the Eclipse Public License 2.0 which is available at | ||
6 | * http://www.eclipse.org/legal/epl-2.0. | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.language.linking; | ||
10 | |||
11 | import com.google.inject.Inject; | ||
12 | import org.apache.log4j.Logger; | ||
13 | import org.eclipse.emf.ecore.EClass; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.EReference; | ||
16 | import org.eclipse.xtext.linking.impl.DefaultLinkingService; | ||
17 | import org.eclipse.xtext.linking.impl.IllegalNodeException; | ||
18 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
19 | import org.eclipse.xtext.naming.QualifiedName; | ||
20 | import org.eclipse.xtext.nodemodel.INode; | ||
21 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
22 | import org.eclipse.xtext.scoping.IScope; | ||
23 | |||
24 | import java.util.*; | ||
25 | |||
26 | public class ProblemLinkingService extends DefaultLinkingService { | ||
27 | @Inject | ||
28 | private IQualifiedNameConverter qualifiedNameConverter; | ||
29 | |||
30 | private static final Logger logger = Logger.getLogger(ProblemLinkingService.class); | ||
31 | |||
32 | @Override | ||
33 | public List<EObject> getLinkedObjects(EObject context, EReference ref, INode node) throws IllegalNodeException { | ||
34 | final EClass requiredType = ref.getEReferenceType(); | ||
35 | if (requiredType == null) { | ||
36 | return List.of(); | ||
37 | } | ||
38 | final String crossRefString = getCrossRefNodeAsString(node); | ||
39 | if (crossRefString == null || crossRefString.isEmpty()) { | ||
40 | return List.of(); | ||
41 | } | ||
42 | if (logger.isDebugEnabled()) { | ||
43 | logger.debug("before getLinkedObjects: node: '%s'".formatted(crossRefString)); | ||
44 | } | ||
45 | final IScope scope = getScope(context, ref); | ||
46 | if (scope == null) { | ||
47 | throw new AssertionError(("Scope provider must not return null for context %s, reference %s! Consider to" + | ||
48 | " return IScope.NULLSCOPE instead.").formatted(context, ref)); | ||
49 | } | ||
50 | final QualifiedName qualifiedLinkName = qualifiedNameConverter.toQualifiedName(crossRefString); | ||
51 | final Iterator<IEObjectDescription> iterator = scope.getElements(qualifiedLinkName).iterator(); | ||
52 | StringBuilder debug = null; | ||
53 | final Set<EObject> result = new LinkedHashSet<>(); | ||
54 | if (logger.isDebugEnabled()) { | ||
55 | debug = new StringBuilder() | ||
56 | .append("after getLinkedObjects: node: '") | ||
57 | .append(crossRefString) | ||
58 | .append("' result: "); | ||
59 | } | ||
60 | while (iterator.hasNext()) { | ||
61 | var eObjectDescription = iterator.next(); | ||
62 | if (debug != null) { | ||
63 | debug.append(eObjectDescription).append(", "); | ||
64 | } | ||
65 | result.add(eObjectDescription.getEObjectOrProxy()); | ||
66 | } | ||
67 | if (debug != null) { | ||
68 | logger.debug(debug); | ||
69 | } | ||
70 | return List.copyOf(result); | ||
71 | } | ||
72 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java new file mode 100644 index 00000000..43239ffe --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java | |||
@@ -0,0 +1,263 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2008, 2023 itemis AG (http://www.itemis.eu) and others. | ||
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
4 | * This program and the accompanying materials are made available under the | ||
5 | * terms of the Eclipse Public License 2.0 which is available at | ||
6 | * http://www.eclipse.org/legal/epl-2.0. | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.language.resource; | ||
10 | |||
11 | import com.google.inject.Inject; | ||
12 | import org.apache.log4j.Logger; | ||
13 | import org.eclipse.emf.ecore.EObject; | ||
14 | import org.eclipse.emf.ecore.EReference; | ||
15 | import org.eclipse.xtext.EcoreUtil2; | ||
16 | import org.eclipse.xtext.diagnostics.DiagnosticMessage; | ||
17 | import org.eclipse.xtext.diagnostics.Severity; | ||
18 | import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider; | ||
19 | import org.eclipse.xtext.linking.impl.IllegalNodeException; | ||
20 | import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; | ||
21 | import org.eclipse.xtext.linking.lazy.LazyLinkingResource; | ||
22 | import org.eclipse.xtext.nodemodel.INode; | ||
23 | import org.eclipse.xtext.resource.DerivedStateAwareResource; | ||
24 | import org.eclipse.xtext.util.Triple; | ||
25 | import org.jetbrains.annotations.Nullable; | ||
26 | |||
27 | import java.util.Arrays; | ||
28 | import java.util.List; | ||
29 | import java.util.Objects; | ||
30 | import java.util.Set; | ||
31 | |||
32 | public class ProblemResource extends DerivedStateAwareResource { | ||
33 | private static final Logger log = Logger.getLogger(ProblemResource.class); | ||
34 | |||
35 | @Inject | ||
36 | private ILinkingDiagnosticMessageProvider.Extended linkingDiagnosticMessageProvider; | ||
37 | |||
38 | /** | ||
39 | * Our own version of this field, because the original is not accessible. | ||
40 | */ | ||
41 | private int cyclicLinkingDetectionCounter = 0; | ||
42 | |||
43 | /** | ||
44 | * Tries to resolve a reference and emits a diagnostic if the reference is unresolvable or ambiguous. | ||
45 | * <p> | ||
46 | * This method was copied from {@link LazyLinkingResource#getEObject(String, Triple)}, but we modified it to also | ||
47 | * handle ambiguous references. | ||
48 | * | ||
49 | * @param uriFragment The URI fragment to resolve. | ||
50 | * @param triple The linking triple. | ||
51 | * @return The resolved {@link EObject}. | ||
52 | * @throws AssertionError If the URI fragment is unresolvable. | ||
53 | */ | ||
54 | @Override | ||
55 | protected EObject getEObject(String uriFragment, Triple<EObject, EReference, INode> triple) throws AssertionError { | ||
56 | cyclicLinkingDetectionCounter++; | ||
57 | if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit && !resolving.add(triple)) { | ||
58 | return handleCyclicResolution(triple); | ||
59 | } | ||
60 | try { | ||
61 | Set<String> unresolvableProxies = getUnresolvableURIFragments(); | ||
62 | if (unresolvableProxies.contains(uriFragment)) { | ||
63 | return null; | ||
64 | } | ||
65 | var result = doGetEObject(triple); | ||
66 | if (result == null) { | ||
67 | if (isUnresolveableProxyCacheable(triple)) { | ||
68 | unresolvableProxies.add(uriFragment); | ||
69 | } | ||
70 | } else { | ||
71 | // remove previously added error markers, since everything should be fine now | ||
72 | unresolvableProxies.remove(uriFragment); | ||
73 | } | ||
74 | return result; | ||
75 | } catch (IllegalNodeException e) { | ||
76 | createAndAddDiagnostic(triple, e); | ||
77 | return null; | ||
78 | } finally { | ||
79 | if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit) { | ||
80 | resolving.remove(triple); | ||
81 | } | ||
82 | cyclicLinkingDetectionCounter--; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | @Nullable | ||
87 | private EObject doGetEObject(Triple<EObject, EReference, INode> triple) { | ||
88 | EReference reference = triple.getSecond(); | ||
89 | try { | ||
90 | List<EObject> linkedObjects = getLinkingService().getLinkedObjects(triple.getFirst(), reference, | ||
91 | triple.getThird()); | ||
92 | if (linkedObjects.isEmpty()) { | ||
93 | createAndAddDiagnostic(triple); | ||
94 | return null; | ||
95 | } | ||
96 | if (linkedObjects.size() > 1) { | ||
97 | createAndAddAmbiguousReferenceDiagnostic(triple); | ||
98 | return null; | ||
99 | } | ||
100 | EObject result = linkedObjects.get(0); | ||
101 | if (!EcoreUtil2.isAssignableFrom(reference.getEReferenceType(), result.eClass())) { | ||
102 | log.error("An element of type %s is not assignable to the reference %s.%s".formatted( | ||
103 | result.getClass().getName(), reference.getEContainingClass().getName(), reference.getName())); | ||
104 | createAndAddDiagnostic(triple); | ||
105 | return null; | ||
106 | } | ||
107 | removeDiagnostic(triple); | ||
108 | return result; | ||
109 | } catch (CyclicLinkingError e) { | ||
110 | if (e.triple.equals(triple)) { | ||
111 | log.error(e.getMessage(), e); | ||
112 | createAndAddDiagnostic(triple); | ||
113 | return null; | ||
114 | } else { | ||
115 | throw e; | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | |||
120 | @Override | ||
121 | protected EObject handleCyclicResolution(Triple<EObject, EReference, INode> triple) throws AssertionError { | ||
122 | // Throw our own version of {@link LazyLinkingResource.CyclicLinkingException}. | ||
123 | throw new CyclicLinkingError("Cyclic resolution of lazy links : %s in resource '%s'.".formatted( | ||
124 | getReferences(triple, resolving), getURI()), triple); | ||
125 | } | ||
126 | |||
127 | @Override | ||
128 | protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
129 | if (isValidationDisabled()) { | ||
130 | return; | ||
131 | } | ||
132 | DiagnosticMessage message = createDiagnosticMessage(triple); | ||
133 | addOrReplaceDiagnostic(triple, message); | ||
134 | } | ||
135 | |||
136 | @Override | ||
137 | protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple, IllegalNodeException ex) { | ||
138 | if (isValidationDisabled()) { | ||
139 | return; | ||
140 | } | ||
141 | ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple); | ||
142 | DiagnosticMessage message = linkingDiagnosticMessageProvider.getIllegalNodeMessage(context, ex); | ||
143 | addOrReplaceDiagnostic(triple, message); | ||
144 | } | ||
145 | |||
146 | protected void createAndAddAmbiguousReferenceDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
147 | if (isValidationDisabled()) { | ||
148 | return; | ||
149 | } | ||
150 | var context = createDiagnosticMessageContext(triple); | ||
151 | var typeName = context.getReference().getEReferenceType().getName(); | ||
152 | String linkText = ""; | ||
153 | try { | ||
154 | linkText = context.getLinkText(); | ||
155 | } catch (IllegalNodeException e) { | ||
156 | linkText = e.getNode().getText(); | ||
157 | } | ||
158 | var messageString = "Ambiguous reference to %s '%s'.".formatted(typeName, linkText); | ||
159 | var message = new DiagnosticMessage(messageString, Severity.ERROR, | ||
160 | org.eclipse.xtext.diagnostics.Diagnostic.LINKING_DIAGNOSTIC); | ||
161 | addOrReplaceDiagnostic(triple, message); | ||
162 | } | ||
163 | |||
164 | /** | ||
165 | * Adds a diagnostic message while maintaining the invariant that at most one | ||
166 | * {@link ProblemResourceLinkingDiagnostic} is added to the {@link #getErrors()} list. | ||
167 | * | ||
168 | * @param triple The triple to add the diagnostic for. | ||
169 | * @param message The diagnostic message. Must have {@link Severity#ERROR}. | ||
170 | */ | ||
171 | protected void addOrReplaceDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) { | ||
172 | if (message == null) { | ||
173 | return; | ||
174 | } | ||
175 | if (message.getSeverity() != Severity.ERROR) { | ||
176 | throw new IllegalArgumentException("Only linking diagnostics of ERROR severity are supported"); | ||
177 | } | ||
178 | var list = getDiagnosticList(message); | ||
179 | var iterator = list.iterator(); | ||
180 | while (iterator.hasNext()) { | ||
181 | var diagnostic = iterator.next(); | ||
182 | if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic && | ||
183 | linkingDiagnostic.matchesNode(triple.getThird())) { | ||
184 | if (linkingDiagnostic.matchesMessage(message)) { | ||
185 | return; | ||
186 | } | ||
187 | iterator.remove(); | ||
188 | break; | ||
189 | } | ||
190 | } | ||
191 | var diagnostic = createDiagnostic(triple, message); | ||
192 | list.add(diagnostic); | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Removes the {@link ProblemResourceLinkingDiagnostic} corresponding to the given node, if prevesent, from the | ||
197 | * {@link #getErrors()} list. | ||
198 | * | ||
199 | * @param triple The triple to add the diagnostic for. | ||
200 | */ | ||
201 | @Override | ||
202 | protected void removeDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
203 | if (getErrors().isEmpty()) { | ||
204 | return; | ||
205 | } | ||
206 | var list = getErrors(); | ||
207 | if (list.isEmpty()) { | ||
208 | return; | ||
209 | } | ||
210 | var iterator = list.iterator(); | ||
211 | while (iterator.hasNext()) { | ||
212 | var diagnostic = iterator.next(); | ||
213 | if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic && | ||
214 | linkingDiagnostic.matchesNode(triple.getThird())) { | ||
215 | iterator.remove(); | ||
216 | return; | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | |||
221 | @Override | ||
222 | protected Diagnostic createDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) { | ||
223 | return new ProblemResourceLinkingDiagnostic(triple.getThird(), message.getMessage(), | ||
224 | message.getIssueCode(), message.getIssueData()); | ||
225 | } | ||
226 | |||
227 | /** | ||
228 | * Our own version of {@link LazyLinkingResource.CyclicLinkingException}, because the {@code tripe} field in the | ||
229 | * original one is not accessible. | ||
230 | * <p> | ||
231 | * Renamed from {@code CyclicLinkingException} to satisfy naming conventions enforced by Sonar. | ||
232 | */ | ||
233 | public static final class CyclicLinkingError extends AssertionError { | ||
234 | private final transient Triple<EObject, EReference, INode> triple; | ||
235 | |||
236 | private CyclicLinkingError(Object detailMessage, Triple<EObject, EReference, INode> triple) { | ||
237 | super(detailMessage); | ||
238 | this.triple = triple; | ||
239 | } | ||
240 | } | ||
241 | |||
242 | /** | ||
243 | * Marks all diagnostics inserted by {@link ProblemResource} with a common superclass so that they can | ||
244 | * later be removed. | ||
245 | * <p> | ||
246 | * We have to inherit from {@link XtextLinkingDiagnostic} to access the protected function {@link #getNode()}. | ||
247 | */ | ||
248 | protected static class ProblemResourceLinkingDiagnostic extends XtextLinkingDiagnostic { | ||
249 | public ProblemResourceLinkingDiagnostic(INode node, String message, String code, String... data) { | ||
250 | super(node, message, code, data); | ||
251 | } | ||
252 | |||
253 | public boolean matchesNode(INode node) { | ||
254 | return Objects.equals(getNode(), node); | ||
255 | } | ||
256 | |||
257 | public boolean matchesMessage(DiagnosticMessage message) { | ||
258 | return Objects.equals(getMessage(), message.getMessage()) && | ||
259 | Objects.equals(getCode(), message.getIssueCode()) && | ||
260 | Arrays.equals(getData(), message.getIssueData()); | ||
261 | } | ||
262 | } | ||
263 | } | ||
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 88d50c5b..56a934cf 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 | |||
@@ -55,7 +55,7 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
55 | var variableOrNode = expr.getVariableOrNode(); | 55 | var variableOrNode = expr.getVariableOrNode(); |
56 | if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { | 56 | if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { |
57 | var name = node.getName(); | 57 | var name = node.getName(); |
58 | var message = ("Only individual nodes can be referenced in predicates. " + | 58 | var message = ("Only individuals can be referenced in predicates. " + |
59 | "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); | 59 | "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); |
60 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, | 60 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, |
61 | INSIGNIFICANT_INDEX, NON_INDIVIDUAL_NODE_ISSUE); | 61 | INSIGNIFICANT_INDEX, NON_INDIVIDUAL_NODE_ISSUE); |
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 new file mode 100644 index 00000000..b1b24ef3 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java | |||
@@ -0,0 +1,94 @@ | |||
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.linking; | ||
7 | |||
8 | |||
9 | import com.google.inject.Inject; | ||
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 | |||
18 | import static org.hamcrest.MatcherAssert.assertThat; | ||
19 | import static org.hamcrest.Matchers.*; | ||
20 | |||
21 | @ExtendWith(InjectionExtension.class) | ||
22 | @InjectWith(ProblemInjectorProvider.class) | ||
23 | class AmbiguousReferenceTest { | ||
24 | @Inject | ||
25 | private ProblemParseHelper parseHelper; | ||
26 | |||
27 | @ParameterizedTest | ||
28 | @ValueSource(strings = {""" | ||
29 | class Foo { | ||
30 | contains Quux quux | ||
31 | } | ||
32 | |||
33 | class Quux. | ||
34 | |||
35 | quux(f, q). | ||
36 | """, """ | ||
37 | class Foo { | ||
38 | contains Quux quux | ||
39 | } | ||
40 | |||
41 | class Quux. | ||
42 | |||
43 | pred example(Foo f, Quux q) <-> quux(f, q). | ||
44 | """, """ | ||
45 | class Foo { | ||
46 | contains Quux quux opposite foo | ||
47 | } | ||
48 | |||
49 | class Bar { | ||
50 | contains Quux quux opposite bar | ||
51 | } | ||
52 | |||
53 | class Quux { | ||
54 | container Foo foo opposite quux | ||
55 | container Bar bar opposite quux | ||
56 | } | ||
57 | """}) | ||
58 | void unambiguousReferenceTest(String text) { | ||
59 | var problem = parseHelper.parse(text); | ||
60 | assertThat(problem.errors(), empty()); | ||
61 | } | ||
62 | |||
63 | @ParameterizedTest | ||
64 | @ValueSource(strings = {""" | ||
65 | class Foo { | ||
66 | contains Quux quux | ||
67 | } | ||
68 | |||
69 | class Bar { | ||
70 | contains Quux quux | ||
71 | } | ||
72 | |||
73 | class Quux. | ||
74 | |||
75 | quux(f, q). | ||
76 | """, """ | ||
77 | class Foo { | ||
78 | contains Quux quux | ||
79 | } | ||
80 | |||
81 | class Bar { | ||
82 | contains Quux quux | ||
83 | } | ||
84 | |||
85 | class Quux. | ||
86 | |||
87 | pred example(Foo f, Quuq q) <-> quux(f, q). | ||
88 | """}) | ||
89 | void ambiguousReferenceTest(String text) { | ||
90 | var problem = parseHelper.parse(text); | ||
91 | assertThat(problem.errors(), hasItem(hasProperty("message", stringContainsInOrder( | ||
92 | "Ambiguous reference", "'quux'")))); | ||
93 | } | ||
94 | } | ||