diff options
author | 2023-12-23 14:39:44 +0100 | |
---|---|---|
committer | 2023-12-24 17:32:54 +0100 | |
commit | ad86c08c451a907803f855533fa7a0876973f70e (patch) | |
tree | df18092bd7101fa2fbcf104a80b0e2e0141cd0dd /subprojects/language/src | |
parent | feat: solution serializer (diff) | |
download | refinery-ad86c08c451a907803f855533fa7a0876973f70e.tar.gz refinery-ad86c08c451a907803f855533fa7a0876973f70e.tar.zst refinery-ad86c08c451a907803f855533fa7a0876973f70e.zip |
fix(language): unambigous reference serialization
Diffstat (limited to 'subprojects/language/src')
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; | |||
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.serializer.tokens.ICrossReferenceSerializer; | ||
23 | import org.eclipse.xtext.validation.IDiagnosticConverter; | 24 | import org.eclipse.xtext.validation.IDiagnosticConverter; |
24 | import org.eclipse.xtext.validation.IResourceValidator; | 25 | import org.eclipse.xtext.validation.IResourceValidator; |
25 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; | 26 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; |
@@ -34,12 +35,15 @@ import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | |||
34 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; | 35 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; |
35 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; | 36 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; |
36 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; | 37 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; |
38 | import tools.refinery.language.serializer.ProblemCrossReferenceSerializer; | ||
37 | import tools.refinery.language.validation.ProblemDiagnosticConverter; | 39 | import 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") | ||
43 | public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | 47 | public 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 | *******************************************************************************/ | ||
9 | package tools.refinery.language.serializer; | ||
10 | |||
11 | import com.google.common.collect.Lists; | ||
12 | import com.google.inject.Inject; | ||
13 | import org.eclipse.emf.common.util.URI; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.EReference; | ||
16 | import org.eclipse.xtext.CrossReference; | ||
17 | import org.eclipse.xtext.EcoreUtil2; | ||
18 | import org.eclipse.xtext.GrammarUtil; | ||
19 | import org.eclipse.xtext.IGrammarAccess; | ||
20 | import org.eclipse.xtext.conversion.IValueConverterService; | ||
21 | import org.eclipse.xtext.conversion.ValueConverterException; | ||
22 | import org.eclipse.xtext.linking.impl.LinkingHelper; | ||
23 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
24 | import org.eclipse.xtext.naming.QualifiedName; | ||
25 | import org.eclipse.xtext.nodemodel.INode; | ||
26 | import org.eclipse.xtext.scoping.IScope; | ||
27 | import org.eclipse.xtext.scoping.IScopeProvider; | ||
28 | import org.eclipse.xtext.serializer.diagnostic.ISerializationDiagnostic; | ||
29 | import org.eclipse.xtext.serializer.diagnostic.SerializationDiagnostic; | ||
30 | import org.eclipse.xtext.serializer.tokens.CrossReferenceSerializer; | ||
31 | import org.eclipse.xtext.serializer.tokens.SerializerScopeProviderBinding; | ||
32 | import org.eclipse.xtext.util.EmfFormatter; | ||
33 | |||
34 | import java.util.List; | ||
35 | |||
36 | public 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 | } |