aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-11-05 20:43:55 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-11-17 12:41:33 +0100
commit31baf36ddbdd3f34318b7bd22c6e773fbdc846ad (patch)
treec35e7517abee69c307e6f35908a85a6fc80ebf3e
parentbuild: prepare for Maven publication (diff)
downloadrefinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.tar.gz
refinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.tar.zst
refinery-31baf36ddbdd3f34318b7bd22c6e773fbdc846ad.zip
feat(langugage): detect ambiguous references
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java10
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java72
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java263
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java2
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java94
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;
12import com.google.inject.Binder; 12import com.google.inject.Binder;
13import com.google.inject.name.Names; 13import com.google.inject.name.Names;
14import org.eclipse.xtext.conversion.IValueConverterService; 14import org.eclipse.xtext.conversion.IValueConverterService;
15import org.eclipse.xtext.linking.ILinkingService;
15import org.eclipse.xtext.naming.IQualifiedNameConverter; 16import org.eclipse.xtext.naming.IQualifiedNameConverter;
16import org.eclipse.xtext.parser.IParser; 17import org.eclipse.xtext.parser.IParser;
17import org.eclipse.xtext.resource.*; 18import org.eclipse.xtext.resource.*;
@@ -22,10 +23,12 @@ import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
22import org.eclipse.xtext.validation.IResourceValidator; 23import org.eclipse.xtext.validation.IResourceValidator;
23import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; 24import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator;
24import tools.refinery.language.conversion.ProblemValueConverterService; 25import tools.refinery.language.conversion.ProblemValueConverterService;
26import tools.refinery.language.linking.ProblemLinkingService;
25import tools.refinery.language.naming.ProblemQualifiedNameConverter; 27import tools.refinery.language.naming.ProblemQualifiedNameConverter;
26import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; 28import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser;
27import tools.refinery.language.resource.ProblemDerivedStateComputer; 29import tools.refinery.language.resource.ProblemDerivedStateComputer;
28import tools.refinery.language.resource.ProblemLocationInFileProvider; 30import tools.refinery.language.resource.ProblemLocationInFileProvider;
31import tools.refinery.language.resource.ProblemResource;
29import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; 32import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
30import tools.refinery.language.scoping.ProblemGlobalScopeProvider; 33import tools.refinery.language.scoping.ProblemGlobalScopeProvider;
31import tools.refinery.language.scoping.ProblemLocalScopeProvider; 34import 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 *******************************************************************************/
9package tools.refinery.language.linking;
10
11import com.google.inject.Inject;
12import org.apache.log4j.Logger;
13import org.eclipse.emf.ecore.EClass;
14import org.eclipse.emf.ecore.EObject;
15import org.eclipse.emf.ecore.EReference;
16import org.eclipse.xtext.linking.impl.DefaultLinkingService;
17import org.eclipse.xtext.linking.impl.IllegalNodeException;
18import org.eclipse.xtext.naming.IQualifiedNameConverter;
19import org.eclipse.xtext.naming.QualifiedName;
20import org.eclipse.xtext.nodemodel.INode;
21import org.eclipse.xtext.resource.IEObjectDescription;
22import org.eclipse.xtext.scoping.IScope;
23
24import java.util.*;
25
26public 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 *******************************************************************************/
9package tools.refinery.language.resource;
10
11import com.google.inject.Inject;
12import org.apache.log4j.Logger;
13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.EReference;
15import org.eclipse.xtext.EcoreUtil2;
16import org.eclipse.xtext.diagnostics.DiagnosticMessage;
17import org.eclipse.xtext.diagnostics.Severity;
18import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider;
19import org.eclipse.xtext.linking.impl.IllegalNodeException;
20import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
21import org.eclipse.xtext.linking.lazy.LazyLinkingResource;
22import org.eclipse.xtext.nodemodel.INode;
23import org.eclipse.xtext.resource.DerivedStateAwareResource;
24import org.eclipse.xtext.util.Triple;
25import org.jetbrains.annotations.Nullable;
26
27import java.util.Arrays;
28import java.util.List;
29import java.util.Objects;
30import java.util.Set;
31
32public 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 */
6package tools.refinery.language.tests.linking;
7
8
9import com.google.inject.Inject;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.extension.ExtendWith;
13import org.junit.jupiter.params.ParameterizedTest;
14import org.junit.jupiter.params.provider.ValueSource;
15import tools.refinery.language.model.tests.utils.ProblemParseHelper;
16import tools.refinery.language.tests.ProblemInjectorProvider;
17
18import static org.hamcrest.MatcherAssert.assertThat;
19import static org.hamcrest.Matchers.*;
20
21@ExtendWith(InjectionExtension.class)
22@InjectWith(ProblemInjectorProvider.class)
23class 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}