aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-23 14:39:44 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-24 17:32:54 +0100
commitad86c08c451a907803f855533fa7a0876973f70e (patch)
treedf18092bd7101fa2fbcf104a80b0e2e0141cd0dd
parentfeat: solution serializer (diff)
downloadrefinery-ad86c08c451a907803f855533fa7a0876973f70e.tar.gz
refinery-ad86c08c451a907803f855533fa7a0876973f70e.tar.zst
refinery-ad86c08c451a907803f855533fa7a0876973f70e.zip
fix(language): unambigous reference serialization
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java8
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemCrossReferenceSerializer.java167
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java75
4 files changed, 246 insertions, 6 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 0a5cb3c2..00dd3de3 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;
20import org.eclipse.xtext.scoping.IScopeProvider; 20import org.eclipse.xtext.scoping.IScopeProvider;
21import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; 21import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider;
22import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; 22import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
23import org.eclipse.xtext.serializer.tokens.ICrossReferenceSerializer;
23import org.eclipse.xtext.validation.IDiagnosticConverter; 24import org.eclipse.xtext.validation.IDiagnosticConverter;
24import org.eclipse.xtext.validation.IResourceValidator; 25import org.eclipse.xtext.validation.IResourceValidator;
25import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; 26import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator;
@@ -34,12 +35,15 @@ import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
34import tools.refinery.language.scoping.ProblemGlobalScopeProvider; 35import tools.refinery.language.scoping.ProblemGlobalScopeProvider;
35import tools.refinery.language.scoping.ProblemLocalScopeProvider; 36import tools.refinery.language.scoping.ProblemLocalScopeProvider;
36import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; 37import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer;
38import tools.refinery.language.serializer.ProblemCrossReferenceSerializer;
37import tools.refinery.language.validation.ProblemDiagnosticConverter; 39import tools.refinery.language.validation.ProblemDiagnosticConverter;
38 40
39/** 41/**
40 * Use this class to register components to be used at runtime / without the 42 * Use this class to register components to be used at runtime / without the
41 * Equinox extension registry. 43 * Equinox extension registry.
42 */ 44 */
45// Unused methods in this class are called by reflection to configure the Xtext Injector.
46@SuppressWarnings("unused")
43public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { 47public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
44 @Override 48 @Override
45 public Class<? extends IParser> bindIParser() { 49 public Class<? extends IParser> bindIParser() {
@@ -104,6 +108,10 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
104 return PreferShortAssertionsProblemSemanticSequencer.class; 108 return PreferShortAssertionsProblemSemanticSequencer.class;
105 } 109 }
106 110
111 public Class<? extends ICrossReferenceSerializer> bindICrossReferenceSerializer() {
112 return ProblemCrossReferenceSerializer.class;
113 }
114
107 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { 115 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() {
108 return ProblemDiagnosticConverter.class; 116 return ProblemDiagnosticConverter.class;
109 } 117 }
diff --git a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
index 770a1a19..0f3bd3ee 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
@@ -69,6 +69,7 @@ public class ProblemFormatter extends AbstractJavaFormatter {
69 doc.prepend(region.keyword("{"), this::oneSpace); 69 doc.prepend(region.keyword("{"), this::oneSpace);
70 doc.append(region.keyword("{"), it -> it.setNewLines(1, 1, 2)); 70 doc.append(region.keyword("{"), it -> it.setNewLines(1, 1, 2));
71 doc.prepend(region.keyword("}"), it -> it.setNewLines(1, 1, 2)); 71 doc.prepend(region.keyword("}"), it -> it.setNewLines(1, 1, 2));
72 doc.interior(region.keyword("{"), region.keyword("}"), IHiddenRegionFormatter::indent);
72 doc.prepend(region.keyword("."), this::noSpace); 73 doc.prepend(region.keyword("."), this::noSpace);
73 for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { 74 for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) {
74 doc.format(featureDeclaration); 75 doc.format(featureDeclaration);
@@ -82,6 +83,7 @@ public class ProblemFormatter extends AbstractJavaFormatter {
82 doc.prepend(region.keyword("{"), this::oneSpace); 83 doc.prepend(region.keyword("{"), this::oneSpace);
83 doc.append(region.keyword("{"), it -> it.setNewLines(1, 1, 2)); 84 doc.append(region.keyword("{"), it -> it.setNewLines(1, 1, 2));
84 doc.prepend(region.keyword("}"), it -> it.setNewLines(1, 1, 2)); 85 doc.prepend(region.keyword("}"), it -> it.setNewLines(1, 1, 2));
86 doc.interior(region.keyword("{"), region.keyword("}"), IHiddenRegionFormatter::indent);
85 doc.prepend(region.keyword("."), this::noSpace); 87 doc.prepend(region.keyword("."), this::noSpace);
86 } 88 }
87 89
diff --git a/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemCrossReferenceSerializer.java b/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemCrossReferenceSerializer.java
new file mode 100644
index 00000000..42950e61
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemCrossReferenceSerializer.java
@@ -0,0 +1,167 @@
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.serializer;
10
11import com.google.common.collect.Lists;
12import com.google.inject.Inject;
13import org.eclipse.emf.common.util.URI;
14import org.eclipse.emf.ecore.EObject;
15import org.eclipse.emf.ecore.EReference;
16import org.eclipse.xtext.CrossReference;
17import org.eclipse.xtext.EcoreUtil2;
18import org.eclipse.xtext.GrammarUtil;
19import org.eclipse.xtext.IGrammarAccess;
20import org.eclipse.xtext.conversion.IValueConverterService;
21import org.eclipse.xtext.conversion.ValueConverterException;
22import org.eclipse.xtext.linking.impl.LinkingHelper;
23import org.eclipse.xtext.naming.IQualifiedNameConverter;
24import org.eclipse.xtext.naming.QualifiedName;
25import org.eclipse.xtext.nodemodel.INode;
26import org.eclipse.xtext.scoping.IScope;
27import org.eclipse.xtext.scoping.IScopeProvider;
28import org.eclipse.xtext.serializer.diagnostic.ISerializationDiagnostic;
29import org.eclipse.xtext.serializer.diagnostic.SerializationDiagnostic;
30import org.eclipse.xtext.serializer.tokens.CrossReferenceSerializer;
31import org.eclipse.xtext.serializer.tokens.SerializerScopeProviderBinding;
32import org.eclipse.xtext.util.EmfFormatter;
33
34import java.util.List;
35
36public class ProblemCrossReferenceSerializer extends CrossReferenceSerializer {
37 public static final String AMBIGUOUS_REFERENCE_DIAGNOSTIC = "tools.refinery.language.serializer" +
38 ".ProblemCrossReferenceSerializer.AMBIGUOUS_REFERENCE";
39
40 @Inject
41 private LinkingHelper linkingHelper;
42
43 @Inject
44 private IQualifiedNameConverter qualifiedNameConverter;
45
46 @Inject
47 @SerializerScopeProviderBinding
48 private IScopeProvider scopeProvider;
49
50 @Inject
51 private IValueConverterService valueConverter;
52
53 @Inject
54 private IGrammarAccess grammarAccess;
55
56 @Override
57 public String serializeCrossRef(EObject semanticObject, CrossReference crossref, EObject target, INode node,
58 ISerializationDiagnostic.Acceptor errors) {
59 if ((target == null || target.eIsProxy()) && node != null) {
60 return tokenUtil.serializeNode(node);
61 }
62
63 final EReference ref = GrammarUtil.getReference(crossref, semanticObject.eClass());
64 final IScope scope = scopeProvider.getScope(semanticObject, ref);
65 if (scope == null) {
66 if (errors != null) {
67 errors.accept(diagnostics.getNoScopeFoundDiagnostic(semanticObject, crossref, target));
68 }
69 return null;
70 }
71
72 if (target != null && target.eIsProxy()) {
73 target = handleProxy(target, semanticObject, ref);
74 }
75
76 if (target != null && node != null) {
77 String text = linkingHelper.getCrossRefNodeAsString(node, true);
78 QualifiedName qualifiedName = qualifiedNameConverter.toQualifiedName(text);
79 URI targetUri = EcoreUtil2.getPlatformResourceOrNormalizedURI(target);
80 if (isUniqueInScope(scope, qualifiedName, targetUri)) {
81 return tokenUtil.serializeNode(node);
82 }
83 }
84
85 return getCrossReferenceNameFromScope(semanticObject, crossref, target, scope, errors);
86 }
87
88 private boolean isUniqueInScope(IScope scope, QualifiedName qualifiedName, URI targetUri) {
89 var iterator = scope.getElements(qualifiedName).iterator();
90 if (!iterator.hasNext()) {
91 return false;
92 }
93 var description = iterator.next();
94 return targetUri.equals(description.getEObjectURI()) && !iterator.hasNext();
95 }
96
97 @Override
98 protected String getCrossReferenceNameFromScope(EObject semanticObject, CrossReference crossref, EObject target,
99 IScope scope, ISerializationDiagnostic.Acceptor errors) {
100 var ruleName = linkingHelper.getRuleNameFrom(crossref);
101 var targetUri = EcoreUtil2.getPlatformResourceOrNormalizedURI(target);
102 FoundName foundName = FoundName.NONE;
103 int shortestNameLength = Integer.MAX_VALUE;
104 String shortestName = null;
105 List<ISerializationDiagnostic> recordedErrors = null;
106 for (var description : scope.getElements(target)) {
107 var qualifiedName = description.getName();
108 var segmentCount = qualifiedName.getSegmentCount();
109 if (shortestName != null && segmentCount >= shortestNameLength) {
110 continue;
111 }
112 if (isUniqueInScope(scope, qualifiedName, targetUri)) {
113 var unconverted = qualifiedNameConverter.toString(qualifiedName);
114 try {
115 shortestName = valueConverter.toString(unconverted, ruleName);
116 shortestNameLength = segmentCount;
117 foundName = FoundName.VALID;
118 } catch (ValueConverterException e) {
119 if (recordedErrors == null) {
120 recordedErrors = Lists.newArrayList();
121 }
122 recordedErrors.add(diagnostics.getValueConversionExceptionDiagnostic(semanticObject, crossref,
123 unconverted, e));
124 }
125 } else if (foundName == FoundName.NONE) {
126 foundName = FoundName.AMBIGUOUS;
127 }
128 }
129 handleErrors(semanticObject, crossref, target, scope, errors, recordedErrors, foundName);
130 return shortestName;
131 }
132
133 private void handleErrors(
134 EObject semanticObject, CrossReference crossref, EObject target, IScope scope,
135 ISerializationDiagnostic.Acceptor errors, List<ISerializationDiagnostic> recordedErrors,
136 FoundName foundName) {
137 if (errors == null) {
138 return;
139 }
140 if (recordedErrors != null) {
141 recordedErrors.forEach(errors::accept);
142 }
143 if (foundName == FoundName.NONE) {
144 errors.accept(diagnostics.getNoEObjectDescriptionFoundDiagnostic(semanticObject, crossref, target,
145 scope));
146 } else if (foundName == FoundName.AMBIGUOUS) {
147 // Computation of reference name copied from
148 // {@link org.eclipse.xtext.serializer.diagnostic.TokenDiagnosticProvider#getFullReferenceName}.
149 var ref = GrammarUtil.getReference(crossref);
150 var clazz = semanticObject.eClass().getName();
151 if (ref.getEContainingClass() != semanticObject.eClass()) {
152 clazz = ref.getEContainingClass().getName() + "(" + clazz + ")";
153 }
154 var message = "No unambiguous name could be found in scope %s.%s for %s"
155 .formatted(clazz, ref.getName(), EmfFormatter.objPath(target));
156 var diagnostic = new SerializationDiagnostic(AMBIGUOUS_REFERENCE_DIAGNOSTIC, semanticObject, crossref,
157 grammarAccess.getGrammar(), message);
158 errors.accept(diagnostic);
159 }
160 }
161
162 private enum FoundName {
163 NONE,
164 AMBIGUOUS,
165 VALID
166 }
167}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
index 4a3a9ac2..65675b6b 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
@@ -156,6 +156,7 @@ class ProblemSerializerTest {
156 problem.getStatements().add(assertion); 156 problem.getStatements().add(assertion);
157 } 157 }
158 158
159
159 @Test 160 @Test
160 void implicitVariableTest() { 161 void implicitVariableTest() {
161 var pred = ProblemFactory.eINSTANCE.createPredicateDefinition(); 162 var pred = ProblemFactory.eINSTANCE.createPredicateDefinition();
@@ -227,15 +228,77 @@ class ProblemSerializerTest {
227 """); 228 """);
228 } 229 }
229 230
231 @Test
232 void unambiguousNameTest() {
233 createClassAndAssertion("Foo", "foo");
234
235 assertSerializedResult("""
236 class Foo {
237 Foo ref
238 }
239
240 ref(foo, foo).
241 """);
242 }
243
244 @Test
245 void ambiguousNameTest() {
246 createClassAndAssertion("Foo", "foo");
247 createClassAndAssertion("Bar", "bar");
248
249 assertSerializedResult("""
250 class Foo {
251 Foo ref
252 }
253
254 Foo::ref(foo, foo).
255
256 class Bar {
257 Bar ref
258 }
259
260 Bar::ref(bar, bar).
261 """);
262 }
263
264 private void createClassAndAssertion(String className, String nodeName) {
265 var classDeclaration = ProblemFactory.eINSTANCE.createClassDeclaration();
266 classDeclaration.setName(className);
267 var referenceDeclaration = ProblemFactory.eINSTANCE.createReferenceDeclaration();
268 referenceDeclaration.setReferenceType(classDeclaration);
269 referenceDeclaration.setName("ref");
270 classDeclaration.getFeatureDeclarations().add(referenceDeclaration);
271 problem.getStatements().add(classDeclaration);
272 var node = ProblemFactory.eINSTANCE.createNode();
273 node.setName(nodeName);
274 problem.getNodes().add(node);
275 createBinaryAssertion(referenceDeclaration, node, node);
276 }
277
278 private void createBinaryAssertion(Relation relation, Node from, Node to) {
279 var assertion = ProblemFactory.eINSTANCE.createAssertion();
280 assertion.setRelation(relation);
281 var fromArgument = ProblemFactory.eINSTANCE.createNodeAssertionArgument();
282 fromArgument.setNode(from);
283 assertion.getArguments().add(fromArgument);
284 var toArgument = ProblemFactory.eINSTANCE.createNodeAssertionArgument();
285 toArgument.setNode(to);
286 assertion.getArguments().add(toArgument);
287 var value = ProblemFactory.eINSTANCE.createLogicConstant();
288 value.setLogicValue(LogicValue.TRUE);
289 assertion.setValue(value);
290 problem.getStatements().add(assertion);
291 }
292
230 private void assertSerializedResult(String expected) { 293 private void assertSerializedResult(String expected) {
231 String problemString; 294 String problemString;
232 try (var outputStream = new ByteArrayOutputStream()) { 295 try (var outputStream = new ByteArrayOutputStream()) {
233 resource.save(outputStream, Map.of()); 296 resource.save(outputStream, Map.of());
234 problemString = outputStream.toString(); 297 problemString = outputStream.toString();
235 } catch (IOException e) { 298 } catch (IOException e) {
236 throw new AssertionError("Failed to serialize problem", e); 299 throw new AssertionError("Failed to serialize problem", e);
237 } 300 }
238 // Nothing to handle in a test. 301 // Nothing to handle in a test.
239 302
240 assertThat(problemString.replace("\r\n", "\n"), equalTo(expected.replace("\r\n", "\n"))); 303 assertThat(problemString.replace("\r\n", "\n"), equalTo(expected.replace("\r\n", "\n")));
241 } 304 }