/******************************************************************************* * Copyright (c) 2008, 2018 itemis AG (http://www.itemis.eu) and others. * Copyright (c) 2023 The Refinery Authors * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ package tools.refinery.language.serializer; import com.google.common.collect.Lists; import com.google.inject.Inject; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.xtext.CrossReference; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.GrammarUtil; import org.eclipse.xtext.IGrammarAccess; import org.eclipse.xtext.conversion.IValueConverterService; import org.eclipse.xtext.conversion.ValueConverterException; import org.eclipse.xtext.linking.impl.LinkingHelper; import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.IScopeProvider; import org.eclipse.xtext.serializer.diagnostic.ISerializationDiagnostic; import org.eclipse.xtext.serializer.diagnostic.SerializationDiagnostic; import org.eclipse.xtext.serializer.tokens.CrossReferenceSerializer; import org.eclipse.xtext.serializer.tokens.SerializerScopeProviderBinding; import org.eclipse.xtext.util.EmfFormatter; import java.util.List; public class ProblemCrossReferenceSerializer extends CrossReferenceSerializer { public static final String AMBIGUOUS_REFERENCE_DIAGNOSTIC = "tools.refinery.language.serializer" + ".ProblemCrossReferenceSerializer.AMBIGUOUS_REFERENCE"; @Inject private LinkingHelper linkingHelper; @Inject private IQualifiedNameConverter qualifiedNameConverter; @Inject @SerializerScopeProviderBinding private IScopeProvider scopeProvider; @Inject private IValueConverterService valueConverter; @Inject private IGrammarAccess grammarAccess; @Override public String serializeCrossRef(EObject semanticObject, CrossReference crossref, EObject target, INode node, ISerializationDiagnostic.Acceptor errors) { if ((target == null || target.eIsProxy()) && node != null) { return tokenUtil.serializeNode(node); } final EReference ref = GrammarUtil.getReference(crossref, semanticObject.eClass()); final IScope scope = scopeProvider.getScope(semanticObject, ref); if (scope == null) { if (errors != null) { errors.accept(diagnostics.getNoScopeFoundDiagnostic(semanticObject, crossref, target)); } return null; } if (target != null && target.eIsProxy()) { target = handleProxy(target, semanticObject, ref); } if (target != null && node != null) { String text = linkingHelper.getCrossRefNodeAsString(node, true); QualifiedName qualifiedName = qualifiedNameConverter.toQualifiedName(text); URI targetUri = EcoreUtil2.getPlatformResourceOrNormalizedURI(target); if (isUniqueInScope(scope, qualifiedName, targetUri)) { return tokenUtil.serializeNode(node); } } return getCrossReferenceNameFromScope(semanticObject, crossref, target, scope, errors); } private boolean isUniqueInScope(IScope scope, QualifiedName qualifiedName, URI targetUri) { var iterator = scope.getElements(qualifiedName).iterator(); if (!iterator.hasNext()) { return false; } var description = iterator.next(); return targetUri.equals(description.getEObjectURI()) && !iterator.hasNext(); } @Override protected String getCrossReferenceNameFromScope(EObject semanticObject, CrossReference crossref, EObject target, IScope scope, ISerializationDiagnostic.Acceptor errors) { var ruleName = linkingHelper.getRuleNameFrom(crossref); var targetUri = EcoreUtil2.getPlatformResourceOrNormalizedURI(target); FoundName foundName = FoundName.NONE; int shortestNameLength = Integer.MAX_VALUE; String shortestName = null; List recordedErrors = null; for (var description : scope.getElements(target)) { var qualifiedName = description.getName(); var segmentCount = qualifiedName.getSegmentCount(); if (shortestName != null && segmentCount >= shortestNameLength) { continue; } if (isUniqueInScope(scope, qualifiedName, targetUri)) { var unconverted = qualifiedNameConverter.toString(qualifiedName); try { shortestName = valueConverter.toString(unconverted, ruleName); shortestNameLength = segmentCount; foundName = FoundName.VALID; } catch (ValueConverterException e) { if (recordedErrors == null) { recordedErrors = Lists.newArrayList(); } recordedErrors.add(diagnostics.getValueConversionExceptionDiagnostic(semanticObject, crossref, unconverted, e)); } } else if (foundName == FoundName.NONE) { foundName = FoundName.AMBIGUOUS; } } handleErrors(semanticObject, crossref, target, scope, errors, recordedErrors, foundName); return shortestName; } private void handleErrors( EObject semanticObject, CrossReference crossref, EObject target, IScope scope, ISerializationDiagnostic.Acceptor errors, List recordedErrors, FoundName foundName) { if (errors == null) { return; } if (recordedErrors != null) { recordedErrors.forEach(errors::accept); } if (foundName == FoundName.NONE) { errors.accept(diagnostics.getNoEObjectDescriptionFoundDiagnostic(semanticObject, crossref, target, scope)); } else if (foundName == FoundName.AMBIGUOUS) { // Computation of reference name copied from // {@link org.eclipse.xtext.serializer.diagnostic.TokenDiagnosticProvider#getFullReferenceName}. var ref = GrammarUtil.getReference(crossref); var clazz = semanticObject.eClass().getName(); if (ref.getEContainingClass() != semanticObject.eClass()) { clazz = ref.getEContainingClass().getName() + "(" + clazz + ")"; } var message = "No unambiguous name could be found in scope %s.%s for %s" .formatted(clazz, ref.getName(), EmfFormatter.objPath(target)); var diagnostic = new SerializationDiagnostic(AMBIGUOUS_REFERENCE_DIAGNOSTIC, semanticObject, crossref, grammarAccess.getGrammar(), message); errors.accept(diagnostic); } } private enum FoundName { NONE, AMBIGUOUS, VALID } }