| /* |
| * Copyright (c) 2016 Google Inc. |
| * |
| * All rights reserved. This program and the accompanying materials are |
| * made available under the terms of the Eclipse Public License v1.0 which |
| * accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| */ |
| package com.google.eclipse.protobuf.scoping; |
| |
| import static com.google.eclipse.protobuf.util.Tracer.DEBUG_SCOPING; |
| import static com.google.eclipse.protobuf.validation.ProtobufResourceValidator.getScopeProviderTimingCollector; |
| import static java.util.Collections.singletonList; |
| |
| import com.google.eclipse.protobuf.naming.ProtobufQualifiedNameConverter; |
| import com.google.eclipse.protobuf.naming.ProtobufQualifiedNameProvider; |
| import com.google.eclipse.protobuf.protobuf.ComplexType; |
| import com.google.eclipse.protobuf.protobuf.ComplexTypeLink; |
| import com.google.eclipse.protobuf.protobuf.ComplexValue; |
| import com.google.eclipse.protobuf.protobuf.ComplexValueField; |
| import com.google.eclipse.protobuf.protobuf.CustomFieldOption; |
| import com.google.eclipse.protobuf.protobuf.CustomOption; |
| import com.google.eclipse.protobuf.protobuf.DefaultValueFieldOption; |
| import com.google.eclipse.protobuf.protobuf.ExtensionFieldName; |
| import com.google.eclipse.protobuf.protobuf.FieldName; |
| import com.google.eclipse.protobuf.protobuf.Group; |
| import com.google.eclipse.protobuf.protobuf.IndexedElement; |
| import com.google.eclipse.protobuf.protobuf.LiteralLink; |
| import com.google.eclipse.protobuf.protobuf.MessageField; |
| import com.google.eclipse.protobuf.protobuf.NativeFieldOption; |
| import com.google.eclipse.protobuf.protobuf.NativeOption; |
| import com.google.eclipse.protobuf.protobuf.OneOf; |
| import com.google.eclipse.protobuf.protobuf.OptionField; |
| import com.google.eclipse.protobuf.protobuf.OptionSource; |
| import com.google.eclipse.protobuf.protobuf.Package; |
| import com.google.eclipse.protobuf.protobuf.Protobuf; |
| import com.google.eclipse.protobuf.protobuf.SimpleValueField; |
| import com.google.eclipse.protobuf.protobuf.TypeLink; |
| import com.google.eclipse.protobuf.protobuf.ValueField; |
| import com.google.eclipse.protobuf.util.EResources; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EReference; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.xtext.naming.QualifiedName; |
| import org.eclipse.xtext.scoping.IScope; |
| import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; |
| import org.eclipse.xtext.scoping.impl.ImportNormalizer; |
| import org.eclipse.xtext.util.IResourceScopeCache; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * A scope provider for the Protobuf language. |
| * |
| * @author atrookey@google.com (Alexander Rookey) |
| */ |
| public class ProtobufScopeProvider extends AbstractDeclarativeScopeProvider { |
| |
| @Inject private ProtoDescriptorProvider descriptorProvider; |
| @Inject private IResourceScopeCache cache; |
| @Inject private ProtobufQualifiedNameConverter nameConverter; |
| @Inject private ProtobufQualifiedNameProvider nameProvider; |
| |
| private ImportNormalizer createImportNormalizerForEObject(EObject element, boolean ignoreCase) { |
| QualifiedName name = nameProvider.getFullyQualifiedName(element); |
| return getLocalScopeProvider().createImportedNamespaceResolver(name.toString(), ignoreCase); |
| } |
| |
| private List<ImportNormalizer> createImportNormalizersForComplexType( |
| ComplexType complexType, boolean ignoreCase) { |
| List<ImportNormalizer> normalizers = new ArrayList<>(); |
| normalizers.add(createImportNormalizerForEObject(complexType, ignoreCase)); |
| normalizers.addAll(createImportNormalizersForOneOf(complexType.eContents(), ignoreCase)); |
| return normalizers; |
| } |
| |
| private List<ImportNormalizer> createImportNormalizersForOneOf( |
| EList<EObject> children, boolean ignoreCase) { |
| List<ImportNormalizer> normalizers = new ArrayList<>(); |
| for (EObject child : children) { |
| if (child instanceof OneOf) { |
| normalizers.add(createImportNormalizerForEObject(child, ignoreCase)); |
| normalizers.addAll(createImportNormalizersForOneOf(child.eContents(), ignoreCase)); |
| } |
| } |
| return normalizers; |
| } |
| |
| /** |
| * An {@code IndexedElement} can be a MessageField or Group. When scoping types {@code FieldName}, |
| * {@code LiteralLink}, or {@code OptionField} that are all related to protocol buffer options, a |
| * scope can be created by traversing the EMF Model to find a suitable {@code IndexedElement}, and |
| * then creating an import normalized scope for the {@code ComplexType} of the {@code |
| * MessageField} or {@code Group}. |
| * |
| * <p>For example: <pre> |
| * enum MyEnum { |
| * FOO = 1; |
| * } |
| * extend google.protobuf.ServiceOptions { |
| * optional MyEnum my_service_option = 50005; |
| * } |
| * service MyService { |
| * option (my_service_option) = FOO; |
| * } |
| * </pre> |
| * |
| * To scope the {@code LiteralLink} {@code FOO} in {@code MyService}, the {@code MessageField} |
| * {@code my_service_option} is found by traversing the model. The method |
| * createNormalizedScopeForIndexedElement(IndexedElement, EReference) creates and returns an |
| * import normalized scope for the type of the {@code MessageField}, {@code MyEnum}. |
| */ |
| private IScope createNormalizedScopeForIndexedElement( |
| IndexedElement indexedElement, EReference reference) { |
| HashMap<EReference, IScope> scopeMap = |
| cache.get( |
| indexedElement, |
| indexedElement.eResource(), |
| new Provider<HashMap<EReference, IScope>>() { |
| @Override |
| public HashMap<EReference, IScope> get() { |
| return new HashMap<>(); |
| } |
| }); |
| if (!scopeMap.containsKey(reference)) { |
| IScope scope = null; |
| if (indexedElement instanceof MessageField) { |
| TypeLink typeLink = ((MessageField) indexedElement).getType(); |
| if (typeLink instanceof ComplexTypeLink) { |
| ComplexType complexType = ((ComplexTypeLink) typeLink).getTarget(); |
| scope = getGlobalScopeProvider().getScope(complexType.eResource(), reference); |
| List<ImportNormalizer> normalizers = |
| createImportNormalizersForComplexType(complexType, false); |
| scope = createProtobufImportScope(scope, complexType, reference, normalizers); |
| } |
| } |
| if (indexedElement instanceof Group) { |
| Group group = (Group) indexedElement; |
| scope = getGlobalScopeProvider().getScope(group.eResource(), reference); |
| ImportNormalizer normalizer = createImportNormalizerForEObject(group, false); |
| scope = createProtobufImportScope(scope, group, reference, singletonList(normalizer)); |
| } |
| scopeMap.put(reference, scope); |
| } |
| return scopeMap.get(reference); |
| } |
| |
| /** |
| * Let the local scope provider compute the list of {@link ImportNormalizer}s to be associated with |
| * the {@link ProtobufImportScope}. |
| */ |
| private IScope createProtobufImportScope(IScope parent, EObject context, EReference reference) { |
| IScope scope = parent; |
| if (context.eContainer() == null) { |
| scope = getLocalScopeProvider().getResourceScope(scope, context, reference); |
| } else { |
| scope = createProtobufImportScope(scope, context.eContainer(), reference); |
| } |
| return getLocalScopeProvider().getLocalElementsScope(scope, context, reference); |
| } |
| |
| /** |
| * Associate the {@link ProtobufImportScope} with the list of {@link ImportNormalizer}s |
| * passed as an argument. |
| */ |
| private IScope createProtobufImportScope( |
| IScope parent, EObject context, EReference reference, List<ImportNormalizer> normalizers) { |
| IScope scope = parent; |
| if (context.eContainer() == null) { |
| scope = getLocalScopeProvider().getResourceScope(scope, context, reference); |
| } else { |
| scope = createProtobufImportScope(scope, context.eContainer(), reference, normalizers); |
| } |
| return getLocalScopeProvider().getLocalElementsScope(scope, context, reference, normalizers); |
| } |
| |
| /** Returns descriptor associated with the current project. */ |
| private @Nullable Resource getDescriptorResource(EObject context) { |
| IProject project = EResources.getProjectOf(context.eResource()); |
| ResourceSet resourceSet = context.eResource().getResourceSet(); |
| ProtoDescriptorProvider.ProtoDescriptorInfo descriptorInfo = |
| descriptorProvider.primaryDescriptor(project); |
| return resourceSet.getResource(descriptorInfo.location, true); |
| } |
| |
| /** Returns the global scope provider. */ |
| private ProtobufImportUriGlobalScopeProvider getGlobalScopeProvider() { |
| return getLocalScopeProvider().getGlobalScopeProvider(); |
| } |
| |
| /** Returns the local scope provider. */ |
| private ProtobufImportedNamespaceAwareLocalScopeProvider getLocalScopeProvider() { |
| return (ProtobufImportedNamespaceAwareLocalScopeProvider) super.getDelegate(); |
| } |
| |
| // TODO (atrookey) Create utility for getting package. |
| private String getPackageOfResource(Resource resource) { |
| return cache.get( |
| "Package", |
| resource, |
| new Provider<String>() { |
| @Override |
| public String get() { |
| Protobuf protobuf; |
| if (resource != null && (protobuf = (Protobuf) resource.getContents().get(0)) != null) { |
| for (EObject content : protobuf.getElements()) { |
| if (content instanceof Package) { |
| return ((Package) content).getImportedNamespace(); |
| } |
| } |
| } |
| return ""; |
| } |
| }); |
| } |
| |
| @Override |
| public IScope getScope(EObject context, EReference reference) { |
| if (DEBUG_SCOPING) { |
| getScopeProviderTimingCollector().startTimer(); |
| } |
| IScope scope = super.getScope(context, reference); |
| if (DEBUG_SCOPING) { |
| getScopeProviderTimingCollector().stopTimer(); |
| } |
| return scope; |
| } |
| |
| /** |
| * Scopes the {@code FieldName}. |
| * |
| * <p>For example: <pre> |
| * message FooOptions { |
| * optional int32 opt1 = 1; |
| * } |
| * extend google.protobuf.FieldOptions { |
| * optional FooOptions foo_options = 1234; |
| * } |
| * message Bar { |
| * optional int32 b = 1 [(foo_options) = { opt1: 123 }]; |
| * } |
| * </pre> |
| * |
| * The {@code NormalFieldName} {@code opt1} contains a cross-reference to {@code FooOptions.opt1}. |
| */ |
| public IScope scope_FieldName_target(FieldName fieldName, EReference reference) { |
| if (fieldName instanceof ExtensionFieldName) { |
| return getLocalScopeProvider().getResourceScope(fieldName.eResource(), reference); |
| } |
| IndexedElement indexedElement = null; |
| OptionSource optionSource = null; |
| EObject valueField = fieldName.eContainer(); |
| if (valueField instanceof ValueField) { |
| EObject complexValue = valueField.eContainer(); |
| if (complexValue instanceof ComplexValue) { |
| EObject unknownOption = complexValue.eContainer(); |
| if (unknownOption instanceof ComplexValueField) { |
| indexedElement = ((ComplexValueField) unknownOption).getName().getTarget(); |
| } |
| if (unknownOption instanceof NativeFieldOption) { |
| NativeFieldOption nativeFieldOption = (NativeFieldOption) unknownOption; |
| optionSource = nativeFieldOption.getSource(); |
| } |
| if (unknownOption instanceof CustomFieldOption) { |
| CustomFieldOption customFieldOption = (CustomFieldOption) unknownOption; |
| optionSource = customFieldOption.getSource(); |
| } |
| if (unknownOption instanceof NativeOption) { |
| NativeOption option = (NativeOption) unknownOption; |
| optionSource = option.getSource(); |
| } |
| if (unknownOption instanceof CustomOption) { |
| CustomOption option = (CustomOption) unknownOption; |
| optionSource = option.getSource(); |
| } |
| if (optionSource != null) { |
| indexedElement = optionSource.getTarget(); |
| } |
| if (indexedElement instanceof MessageField) { |
| return createNormalizedScopeForIndexedElement(indexedElement, reference); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Creates a scope containing elements of type {@code Literal} that can be referenced with their |
| * local name only. |
| * |
| * <p>For example: <pre> |
| * enum MyEnum { |
| * FOO = 1; |
| * } |
| * extend google.protobuf.ServiceOptions { |
| * optional MyEnum my_service_option = 50005; |
| * } |
| * service MyService { |
| * option (my_service_option) = FOO; |
| * } |
| * </pre> |
| * |
| * The {@code LiteralLink} {@code FOO} contains a cross-reference to {@code MyEnum.FOO}. |
| */ |
| public @Nullable IScope scope_LiteralLink_target(LiteralLink literalLink, EReference reference) { |
| EObject container = literalLink.eContainer(); |
| IndexedElement indexedElement = null; |
| if (container instanceof DefaultValueFieldOption) { |
| container = container.eContainer(); |
| if (container instanceof IndexedElement) { |
| indexedElement = (IndexedElement) container; |
| } |
| } |
| if (container instanceof NativeFieldOption) { |
| indexedElement = ((NativeFieldOption) container).getSource().getTarget(); |
| } |
| if (container instanceof NativeOption) { |
| indexedElement = ((NativeOption) container).getSource().getTarget(); |
| } |
| if (container instanceof CustomFieldOption) { |
| EList<OptionField> fields = ((CustomFieldOption) container).getFields(); |
| if (!fields.isEmpty()) { |
| indexedElement = fields.get(fields.size() - 1).getTarget(); |
| } else { |
| indexedElement = ((CustomFieldOption) container).getSource().getTarget(); |
| } |
| } |
| if (container instanceof CustomOption) { |
| EList<OptionField> fields = ((CustomOption) container).getFields(); |
| if (!fields.isEmpty()) { |
| indexedElement = fields.get(fields.size() - 1).getTarget(); |
| } else { |
| indexedElement = ((CustomOption) container).getSource().getTarget(); |
| } |
| } |
| if (container instanceof SimpleValueField) { |
| indexedElement = ((SimpleValueField) container).getName().getTarget(); |
| } |
| return createNormalizedScopeForIndexedElement(indexedElement, reference); |
| } |
| |
| /** |
| * Recursively scopes the {@code OptionField} starting with the {@code OptionSource}. |
| * |
| * <p>For example: <pre> |
| * message Code { |
| * optional double number = 1; |
| * } |
| * message Type { |
| * optional Code code = 1; |
| * } |
| * extend proto2.FieldOptions { |
| * optional Type type = 1000; |
| * } |
| * message Person { |
| * optional bool active = 1 [(type).code.number = 68]; |
| * } |
| * </pre> |
| * |
| * The {@code OptionField} {@code number} contains a cross-reference to {@code Code.number}. |
| */ |
| public IScope scope_OptionField_target(OptionField optionField, EReference reference) { |
| IScope scope = getLocalScopeProvider().getResourceScope(optionField.eResource(), reference); |
| EObject customOption = optionField.eContainer(); |
| if (customOption != null) { |
| OptionSource optionSource = null; |
| EList<OptionField> fields = null; |
| if (customOption instanceof CustomFieldOption) { |
| optionSource = ((CustomFieldOption) customOption).getSource(); |
| fields = ((CustomFieldOption) customOption).getFields(); |
| } |
| if (customOption instanceof CustomOption) { |
| optionSource = ((CustomOption) customOption).getSource(); |
| fields = ((CustomOption) customOption).getFields(); |
| } |
| if (optionSource != null && fields != null) { |
| int index = fields.indexOf(optionField); |
| if (index < 0 || fields.size() <= index) { |
| throw new IllegalArgumentException( |
| "index is " + index + " but field.size() is " + fields.size()); |
| } |
| IndexedElement indexedElement = null; |
| if (index == 0) { |
| indexedElement = optionSource.getTarget(); |
| } else { |
| indexedElement = fields.get(index - 1).getTarget(); |
| } |
| return createNormalizedScopeForIndexedElement(indexedElement, reference); |
| } |
| } |
| return scope; |
| } |
| |
| /** |
| * Creates a scope containing the default options defined in descriptor.proto. |
| * |
| * <p>For example: <pre> |
| * option java_package = "com.example.foo"; |
| * </pre> |
| * |
| * The {@code OptionSource} {@code java_package} contains a cross-reference to {@code |
| * google.protobuf.FileOptions.java_package} defined in descriptor.proto. |
| */ |
| public IScope scope_OptionSource_target(OptionSource optionSource, EReference reference) { |
| String optionType = OptionType.typeOf(optionSource).messageName(); |
| Resource resource = optionSource.eResource(); |
| IScope descriptorScope = |
| cache.get( |
| optionType, |
| resource, |
| new Provider<IScope>() { |
| @Override |
| public IScope get() { |
| IScope scope = getGlobalScopeProvider().getScope(resource, reference); |
| Resource descriptorResource = getDescriptorResource(optionSource); |
| String descriptorMessage = |
| getPackageOfResource(descriptorResource) |
| + nameConverter.getDelimiter() |
| + optionType; |
| ImportNormalizer normalizer = |
| getLocalScopeProvider() |
| .createImportedNamespaceResolver(descriptorMessage, false); |
| scope = |
| createProtobufImportScope( |
| scope, |
| descriptorResource.getContents().get(0), |
| reference, |
| singletonList(normalizer)); |
| return scope; |
| } |
| }); |
| return createProtobufImportScope(descriptorScope, optionSource, reference); |
| } |
| } |