com.google.eclipse.protobuf.validation.ProtobufJavaValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.eclipse.protobuf.validation.ProtobufJavaValidator.java

Source

/*
 * Copyright (c) 2014, 2015 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.validation;

import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MAP_TYPE__KEY_TYPE;
import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MAP_TYPE__VALUE_TYPE;
import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MESSAGE_FIELD__MODIFIER;
import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.PACKAGE__IMPORTED_NAMESPACE;
import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.SYNTAX__NAME;
import static com.google.eclipse.protobuf.validation.Messages.expectedFieldNumber;
import static com.google.eclipse.protobuf.validation.Messages.expectedSyntaxIdentifier;
import static com.google.eclipse.protobuf.validation.Messages.fieldNumbersMustBePositive;
import static com.google.eclipse.protobuf.validation.Messages.indexRangeEndLessThanStart;
import static com.google.eclipse.protobuf.validation.Messages.indexRangeNonPositive;
import static com.google.eclipse.protobuf.validation.Messages.invalidMapKeyType;
import static com.google.eclipse.protobuf.validation.Messages.invalidMapValueType;
import static com.google.eclipse.protobuf.validation.Messages.mapWithModifier;
import static com.google.eclipse.protobuf.validation.Messages.mapWithinTypeExtension;
import static com.google.eclipse.protobuf.validation.Messages.missingModifier;
import static com.google.eclipse.protobuf.validation.Messages.multiplePackages;
import static com.google.eclipse.protobuf.validation.Messages.nameConflict;
import static com.google.eclipse.protobuf.validation.Messages.oneofFieldWithModifier;
import static com.google.eclipse.protobuf.validation.Messages.requiredInProto3;
import static com.google.eclipse.protobuf.validation.Messages.reservedIndexAndName;
import static com.google.eclipse.protobuf.validation.Messages.reservedToMax;
import static com.google.eclipse.protobuf.validation.Messages.tagNumberRangeConflict;
import static com.google.eclipse.protobuf.validation.Messages.tagNumberConflict;
import static com.google.eclipse.protobuf.validation.Messages.conflictingExtensions;
import static com.google.eclipse.protobuf.validation.Messages.conflictingField;
import static com.google.eclipse.protobuf.validation.Messages.conflictingGroup;
import static com.google.eclipse.protobuf.validation.Messages.conflictingReservedName;
import static com.google.eclipse.protobuf.validation.Messages.conflictingReservedNumber;
import static com.google.eclipse.protobuf.validation.Messages.unknownSyntax;
import static com.google.eclipse.protobuf.validation.Messages.unrecognizedSyntaxIdentifier;
import static java.lang.String.format;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Range;
import com.google.eclipse.protobuf.model.util.IndexRanges;
import com.google.eclipse.protobuf.model.util.IndexRanges.BackwardsRangeException;
import com.google.eclipse.protobuf.model.util.IndexedElements;
import com.google.eclipse.protobuf.model.util.Protobufs;
import com.google.eclipse.protobuf.model.util.StringLiterals;
import com.google.eclipse.protobuf.model.util.Syntaxes;
import com.google.eclipse.protobuf.naming.NameResolver;
import com.google.eclipse.protobuf.protobuf.Extensions;
import com.google.eclipse.protobuf.protobuf.Group;
import com.google.eclipse.protobuf.protobuf.IndexRange;
import com.google.eclipse.protobuf.protobuf.IndexedElement;
import com.google.eclipse.protobuf.protobuf.MapType;
import com.google.eclipse.protobuf.protobuf.MapTypeLink;
import com.google.eclipse.protobuf.protobuf.Message;
import com.google.eclipse.protobuf.protobuf.MessageField;
import com.google.eclipse.protobuf.protobuf.ModifierEnum;
import com.google.eclipse.protobuf.protobuf.OneOf;
import com.google.eclipse.protobuf.protobuf.Package;
import com.google.eclipse.protobuf.protobuf.Protobuf;
import com.google.eclipse.protobuf.protobuf.ProtobufElement;
import com.google.eclipse.protobuf.protobuf.ProtobufPackage;
import com.google.eclipse.protobuf.protobuf.Reservation;
import com.google.eclipse.protobuf.protobuf.Reserved;
import com.google.eclipse.protobuf.protobuf.ScalarType;
import com.google.eclipse.protobuf.protobuf.ScalarTypeLink;
import com.google.eclipse.protobuf.protobuf.StringLiteral;
import com.google.eclipse.protobuf.protobuf.Syntax;
import com.google.eclipse.protobuf.protobuf.TypeExtension;
import com.google.eclipse.protobuf.protobuf.TypeLink;
import com.google.inject.Inject;

import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.util.SimpleAttributeResolver;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.ComposedChecks;

/**
 * @author alruiz@google.com (Alex Ruiz)
 */
@ComposedChecks(validators = { DataTypeValidator.class, ImportValidator.class })
public class ProtobufJavaValidator extends AbstractProtobufJavaValidator {
    public static final String SYNTAX_IS_NOT_KNOWN_ERROR = "syntaxIsNotProto2";
    public static final String INVALID_FIELD_TAG_NUMBER_ERROR = "invalidFieldTagNumber";
    public static final String MORE_THAN_ONE_PACKAGE_ERROR = "moreThanOnePackage";
    public static final String MISSING_MODIFIER_ERROR = "noModifier";
    public static final String MAP_WITH_MODIFIER_ERROR = "mapWithModifier";
    public static final String REQUIRED_IN_PROTO3_ERROR = "requiredInProto3";
    public static final String INVALID_MAP_KEY_TYPE_ERROR = "invalidMapKeyType";
    public static final String MAP_WITH_MAP_VALUE_TYPE_ERROR = "mapWithMapValueType";
    public static final String ONEOF_FIELD_WITH_MODIFIER_ERROR = "oneofFieldWithModifier";

    @Inject
    private IndexedElements indexedElements;
    @Inject
    private IndexRanges indexRanges;
    @Inject
    private NameResolver nameResolver;
    @Inject
    private StringLiterals stringLiterals;
    @Inject
    private Protobufs protobufs;
    @Inject
    private Syntaxes syntaxes;

    @Check
    public void checkIsKnownSyntax(Protobuf protobuf) {
        if (!protobufs.hasKnownSyntax(protobuf)) {
            warning(unknownSyntax, null);
        }
    }

    @Check
    public void checkSyntaxIsKnown(Syntax syntax) {
        if (syntaxes.isSpecifyingProto2Syntax(syntax) || syntaxes.isSpecifyingProto3Syntax(syntax)) {
            return;
        }
        String name = syntaxes.getName(syntax);
        String msg = (name == null) ? expectedSyntaxIdentifier : format(unrecognizedSyntaxIdentifier, name);
        error(msg, syntax, SYNTAX__NAME, SYNTAX_IS_NOT_KNOWN_ERROR);
    }

    @Check
    public void checkIndexRangeBounds(IndexRange indexRange) {
        Range<Long> range;
        try {
            range = indexRanges.toLongRange(indexRange);
        } catch (BackwardsRangeException e) {
            error(indexRangeEndLessThanStart, indexRange, null);
            return;
        }

        if (range.lowerEndpoint() <= 0) {
            error(indexRangeNonPositive, indexRange, ProtobufPackage.Literals.INDEX_RANGE__FROM);
        }
    }

    @Check
    public void checkForIndexConflicts(Message message) {
        Multimap<EObject, Range<Long>> rangeUsages = LinkedHashMultimap.create();

        for (Reserved reserved : getOwnedElements(message, Reserved.class)) {
            for (IndexRange indexRange : Iterables.filter(reserved.getReservations(), IndexRange.class)) {
                try {
                    Range<Long> range = indexRanges.toLongRange(indexRange);
                    errorOnConflicts(range, rangeUsages, indexRange, null);
                    rangeUsages.put(reserved, range);
                } catch (BackwardsRangeException e) {
                    // Do not try to find conflicts with invalid ranges.
                }
            }
        }

        for (Extensions extensions : getOwnedElements(message, Extensions.class)) {
            for (IndexRange indexRange : extensions.getRanges()) {
                try {
                    Range<Long> range = indexRanges.toLongRange(indexRange);
                    errorOnConflicts(range, rangeUsages, indexRange, null);
                    rangeUsages.put(extensions, range);
                } catch (BackwardsRangeException e) {
                    // Do not try to find conflicts with invalid ranges.
                }
            }
        }

        for (IndexedElement element : getOwnedElements(message, IndexedElement.class)) {
            long index = indexedElements.indexOf(element);
            Range<Long> range = Range.singleton(index);
            EStructuralFeature feature = indexedElements.indexFeatureOf(element);
            errorOnConflicts(range, rangeUsages, element, feature);
            rangeUsages.put(element, range);
        }
    }

    private void errorOnConflicts(Range<Long> range, Multimap<EObject, Range<Long>> rangeUsages,
            EObject errorSource, EStructuralFeature errorFeature) {
        for (Map.Entry<EObject, Range<Long>> rangeUsage : rangeUsages.entries()) {
            Range<Long> usedRange = rangeUsage.getValue();
            if (range.isConnected(usedRange)) {
                EObject rangeUser = rangeUsage.getKey();

                boolean rangeIsSingular = range.hasUpperBound() && range.upperEndpoint() == range.lowerEndpoint();
                String template = rangeIsSingular ? tagNumberConflict : tagNumberRangeConflict;

                String rangeUserString;
                String usedRangeString = rangeToString(usedRange);
                if (rangeUser instanceof MessageField) {
                    rangeUserString = String.format(conflictingField, nameResolver.nameOf(rangeUser),
                            usedRangeString);
                } else if (rangeUser instanceof Group) {
                    rangeUserString = String.format(conflictingGroup, nameResolver.nameOf(rangeUser),
                            usedRangeString);
                } else if (rangeUser instanceof Reserved) {
                    rangeUserString = String.format(conflictingReservedNumber, usedRangeString);
                } else {
                    rangeUserString = String.format(conflictingExtensions, usedRangeString);
                }

                String message = String.format(template, rangeToString(range), rangeUserString);
                error(message, errorSource, errorFeature);

                // Don't report more than one error per element.
                return;
            }
        }
    }

    private String rangeToString(Range<Long> range) {
        if (range.hasLowerBound() && range.hasUpperBound() && range.lowerEndpoint() == range.upperEndpoint()) {
            return String.valueOf(range.lowerEndpoint());
        }

        String upper = range.hasUpperBound() ? String.valueOf(range.upperEndpoint()) : indexRanges.getMaxKeyword();
        return String.format("%d to %s", range.lowerEndpoint(), upper);
    }

    @Check
    public void checkForReservedToMax(Reserved reserved) {
        for (IndexRange range : Iterables.filter(reserved.getReservations(), IndexRange.class)) {
            String to = range.getTo();
            if (indexRanges.getMaxKeyword().equals(to)) {
                error(reservedToMax, range, ProtobufPackage.Literals.INDEX_RANGE__TO);
            }
        }
    }

    @Check
    public void checkForReservedNameConflicts(Message message) {
        Set<String> reservedNames = new HashSet<>();
        for (Reserved reserved : getOwnedElements(message, Reserved.class)) {
            for (StringLiteral stringLiteral : Iterables.filter(reserved.getReservations(), StringLiteral.class)) {
                String name = stringLiterals.getCombinedString(stringLiteral);
                reportReservedNameConflicts(name, reservedNames, stringLiteral, null);
                reservedNames.add(name);
            }
        }

        for (IndexedElement element : getOwnedElements(message, IndexedElement.class)) {
            String name = nameResolver.nameOf(element);
            if (name != null) {
                EAttribute nameAttribute = SimpleAttributeResolver.NAME_RESOLVER.getAttribute(element);
                reportReservedNameConflicts(name, reservedNames, element, nameAttribute);
            }
        }
    }

    @Check
    public void checkForReservedIndexAndName(Reserved reserved) {
        boolean hasIndexReservation = false;
        boolean hasNameReservation = false;
        for (Reservation reservation : reserved.getReservations()) {
            if (reservation instanceof IndexRange) {
                hasIndexReservation = true;
            } else if (reservation instanceof StringLiteral) {
                hasNameReservation = true;
            }
        }

        if (hasIndexReservation && hasNameReservation) {
            error(reservedIndexAndName, reserved, null);
        }
    }

    private void reportReservedNameConflicts(String name, Set<String> reservedNames, EObject errorSource,
            EAttribute errorFeature) {
        if (reservedNames.contains(name)) {
            String nameUser = String.format(conflictingReservedName, name);
            String message = String.format(nameConflict, name, nameUser);
            error(message, errorSource, errorFeature);
        }
    }

    /**
     * Returns elements of the given type contained within the given message, except those contained
     * within intervening Messages or TypeExtensions.
     */
    private static <E extends EObject> Collection<E> getOwnedElements(Message container, Class<E> elementType) {
        Collection<E> elements = new ArrayList<E>();

        TreeIterator<EObject> elementsIterator = container.eAllContents();
        while (elementsIterator.hasNext()) {
            EObject element = elementsIterator.next();

            if (elementType.isAssignableFrom(element.getClass())) {
                @SuppressWarnings("unchecked")
                E elementAsElementType = (E) element;
                elements.add(elementAsElementType);
            }

            if (element instanceof Message || element instanceof TypeExtension) {
                elementsIterator.prune();
            }
        }

        return elements;
    }

    @Check
    public void checkFieldModifiers(MessageField field) {
        if (field.getType() instanceof MapTypeLink) {
            checkMapField(field);
            return;
        }
        if (field.eContainer() instanceof OneOf) {
            checkOneOfField(field);
            return;
        }
        if (field.getModifier() == ModifierEnum.UNSPECIFIED && isProto2Field(field)) {
            error(missingModifier, field, MESSAGE_FIELD__MODIFIER, MISSING_MODIFIER_ERROR);
        } else if (field.getModifier() == ModifierEnum.REQUIRED && isProto3Field(field)) {
            error(requiredInProto3, field, MESSAGE_FIELD__MODIFIER, REQUIRED_IN_PROTO3_ERROR);
        }
    }

    private void checkMapField(MessageField field) {
        // TODO(het): Add quickfix to delete the modifier
        if (field.getModifier() != ModifierEnum.UNSPECIFIED) {
            error(mapWithModifier, field, MESSAGE_FIELD__MODIFIER, MAP_WITH_MODIFIER_ERROR);
        }
    }

    private void checkOneOfField(MessageField field) {
        if (field.getModifier() != ModifierEnum.UNSPECIFIED) {
            error(oneofFieldWithModifier, field, MESSAGE_FIELD__MODIFIER, ONEOF_FIELD_WITH_MODIFIER_ERROR);
        }
    }

    private boolean isProto2Field(MessageField field) {
        EObject container = field.eContainer();
        if (container != null && !(container instanceof Protobuf)) {
            container = container.eContainer();
        }
        if (container instanceof Protobuf) {
            return syntaxes.isSpecifyingProto2Syntax(((Protobuf) container).getSyntax());
        }
        return false;
    }

    private boolean isProto3Field(MessageField field) {
        EObject container = field.eContainer();
        while (container != null && !(container instanceof Protobuf)) {
            container = container.eContainer();
        }
        if (container instanceof Protobuf) {
            return syntaxes.isSpecifyingProto3Syntax(((Protobuf) container).getSyntax());
        }
        return false;
    }

    @Check
    public void checkTagNumberIsGreaterThanZero(IndexedElement e) {
        if (isNameNull(e)) {
            return; // we already show an error if name is null, no need to go further.
        }
        long index = indexedElements.indexOf(e);
        if (index > 0) {
            return;
        }
        String msg = (index == 0) ? fieldNumbersMustBePositive : expectedFieldNumber;
        invalidTagNumberError(msg, e);
    }

    private void invalidTagNumberError(String message, IndexedElement e) {
        error(message, e, indexedElements.indexFeatureOf(e), INVALID_FIELD_TAG_NUMBER_ERROR);
    }

    @Check
    public void checkOnlyOnePackageDefinition(Package aPackage) {
        boolean firstFound = false;
        Protobuf root = (Protobuf) aPackage.eContainer();
        for (ProtobufElement e : root.getElements()) {
            if (e == aPackage) {
                if (firstFound) {
                    error(multiplePackages, aPackage, PACKAGE__IMPORTED_NAMESPACE, MORE_THAN_ONE_PACKAGE_ERROR);
                }
                return;
            }
            if (e instanceof Package && !firstFound) {
                firstFound = true;
            }
        }
    }

    private boolean isNameNull(IndexedElement e) {
        return nameResolver.nameOf(e) == null;
    }

    @Check
    public void checkMapTypeHasValidKeyType(MapType map) {
        TypeLink keyType = map.getKeyType();
        if (!(keyType instanceof ScalarTypeLink)) {
            error(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE, INVALID_MAP_KEY_TYPE_ERROR);
            return;
        }
        ScalarType scalarKeyType = ((ScalarTypeLink) keyType).getTarget();
        if (scalarKeyType == ScalarType.BYTES || scalarKeyType == ScalarType.DOUBLE
                || scalarKeyType == ScalarType.FLOAT) {
            error(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE, INVALID_MAP_KEY_TYPE_ERROR);
        }
    }

    @Check
    public void checkMapTypeHasValidValueType(MapType map) {
        TypeLink keyType = map.getValueType();
        if (keyType instanceof MapTypeLink) {
            error(invalidMapValueType, map, MAP_TYPE__VALUE_TYPE, MAP_WITH_MAP_VALUE_TYPE_ERROR);
            return;
        }
    }

    @Check
    public void checkMapIsNotWithinExtension(MapType map) {
        if (EcoreUtil2.getContainerOfType(map, TypeExtension.class) != null) {
            error(mapWithinTypeExtension, map, null);
        }
    }
}