Validate map type fields and allow type linking in map types

Change-Id: I7b6026a7ec81e8f1c3775347c725704fedf82852
Signed-off-by: Harry Terkelsen <het@google.com>
diff --git a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidKeyType_Test.java b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidKeyType_Test.java
new file mode 100644
index 0000000..ef57d47
--- /dev/null
+++ b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidKeyType_Test.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2014 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.junit.core.UnitTestModule.unitTestModule;
+import static com.google.eclipse.protobuf.junit.core.XtextRule.overrideRuntimeModuleWith;
+import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MAP_TYPE__KEY_TYPE;
+import static com.google.eclipse.protobuf.validation.Messages.invalidMapKeyType;
+import static com.google.eclipse.protobuf.validation.ProtobufJavaValidator.INVALID_MAP_KEY_TYPE_ERROR;
+import static org.eclipse.xtext.validation.ValidationMessageAcceptor.INSIGNIFICANT_INDEX;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.google.eclipse.protobuf.junit.core.XtextRule;
+import com.google.eclipse.protobuf.protobuf.MapType;
+import com.google.eclipse.protobuf.protobuf.MapTypeLink;
+import com.google.eclipse.protobuf.protobuf.MessageField;
+import com.google.inject.Inject;
+
+import org.eclipse.xtext.validation.ValidationMessageAcceptor;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Tests for <code>{@link ProtobufJavaValidator#checkMapTypeHasValidKeyType(MapType)}</code>
+ */
+public class ProtobufJavaValidator_checkMapTypeHasValidKeyType_Test {
+  @Rule public XtextRule xtext = overrideRuntimeModuleWith(unitTestModule());
+
+  @Inject private ProtobufJavaValidator validator;
+  private ValidationMessageAcceptor messageAcceptor;
+
+  @Before public void setUp() {
+    messageAcceptor = mock(ValidationMessageAcceptor.class);
+    validator.setMessageAcceptor(messageAcceptor);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Bar {}
+  //
+  // message Foo {
+  //   map<Bar, string> bar = 1;
+  // }
+  @Test public void should_create_error_if_key_type_is_message() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verify(messageAcceptor).acceptError(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE,
+        INSIGNIFICANT_INDEX, INVALID_MAP_KEY_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<double, string> bar = 1;
+  // }
+  @Test public void should_create_error_if_key_type_is_double() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verify(messageAcceptor).acceptError(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE,
+        INSIGNIFICANT_INDEX, INVALID_MAP_KEY_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<float, string> bar = 1;
+  // }
+  @Test public void should_create_error_if_key_type_is_float() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verify(messageAcceptor).acceptError(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE,
+        INSIGNIFICANT_INDEX, INVALID_MAP_KEY_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<bytes, string> bar = 1;
+  // }
+  @Test public void should_create_error_if_key_type_is_bytes() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verify(messageAcceptor).acceptError(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE,
+        INSIGNIFICANT_INDEX, INVALID_MAP_KEY_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<map<string, string>, string> bar = 1;
+  // }
+  @Test public void should_create_error_if_key_type_is_map() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verify(messageAcceptor).acceptError(invalidMapKeyType, map, MAP_TYPE__KEY_TYPE,
+        INSIGNIFICANT_INDEX, INVALID_MAP_KEY_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<bool, string> bar = 1;
+  // }
+  @Test public void should_not_create_error_if_key_type_is_bool() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verifyZeroInteractions(messageAcceptor);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<int32, string> bar = 1;
+  // }
+  @Test public void should_not_create_error_if_key_type_is_scalar() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verifyZeroInteractions(messageAcceptor);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<string, string> bar = 1;
+  // }
+  @Test public void should_not_create_error_if_key_type_is_string() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidKeyType(map);
+    verifyZeroInteractions(messageAcceptor);
+  }
+}
diff --git a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidValueType_Test.java b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidValueType_Test.java
new file mode 100644
index 0000000..1893d66
--- /dev/null
+++ b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator_checkMapTypeHasValidValueType_Test.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2014 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.junit.core.UnitTestModule.unitTestModule;
+import static com.google.eclipse.protobuf.junit.core.XtextRule.overrideRuntimeModuleWith;
+import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MAP_TYPE__VALUE_TYPE;
+import static com.google.eclipse.protobuf.validation.Messages.invalidMapValueType;
+import static com.google.eclipse.protobuf.validation.ProtobufJavaValidator.MAP_WITH_MAP_VALUE_TYPE_ERROR;
+import static org.eclipse.xtext.validation.ValidationMessageAcceptor.INSIGNIFICANT_INDEX;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.google.eclipse.protobuf.junit.core.XtextRule;
+import com.google.eclipse.protobuf.protobuf.MapType;
+import com.google.eclipse.protobuf.protobuf.MapTypeLink;
+import com.google.eclipse.protobuf.protobuf.MessageField;
+import com.google.inject.Inject;
+
+import org.eclipse.xtext.validation.ValidationMessageAcceptor;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Tests for <code>{@link ProtobufJavaValidator#checkMapTypeHasValidValueType(MapType)}</code>
+ */
+public class ProtobufJavaValidator_checkMapTypeHasValidValueType_Test {
+  @Rule public XtextRule xtext = overrideRuntimeModuleWith(unitTestModule());
+
+  @Inject private ProtobufJavaValidator validator;
+  private ValidationMessageAcceptor messageAcceptor;
+
+  @Before public void setUp() {
+    messageAcceptor = mock(ValidationMessageAcceptor.class);
+    validator.setMessageAcceptor(messageAcceptor);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Foo {
+  //   map<string, map<string, string> > bar = 1;
+  // }
+  @Test public void should_create_error_if_value_type_is_map() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidValueType(map);
+    verify(messageAcceptor).acceptError(invalidMapValueType, map, MAP_TYPE__VALUE_TYPE,
+        INSIGNIFICANT_INDEX, MAP_WITH_MAP_VALUE_TYPE_ERROR);
+  }
+
+  // syntax = "proto2";
+  //
+  // message Bar {}
+  //
+  // message Foo {
+  //   map<string, Bar> bar = 1;
+  // }
+  @Test public void should_not_create_error_if_value_type_is_message() {
+    MessageField field = xtext.find("bar", MessageField.class);
+    MapType map = ((MapTypeLink) field.getType()).getTarget();
+    validator.checkMapTypeHasValidValueType(map);
+    verifyZeroInteractions(messageAcceptor);
+  }
+}
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/Protobuf.xtext b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/Protobuf.xtext
index fb0a8e6..6ef7ab7 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/Protobuf.xtext
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/Protobuf.xtext
@@ -107,7 +107,7 @@
   target=MapType;
 
 MapType:
-  ->'map' '<' fromType=TypeLink ',' toType=TypeLink '>';
+  ->'map' '<' keyType=TypeLink ',' valueType=TypeLink '>';
 
 Enum:
   ->'enum' name=Name '{'
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/MessageFields.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/MessageFields.java
index 3c09143..e5d9f34 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/MessageFields.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/MessageFields.java
@@ -31,6 +31,7 @@
 import com.google.eclipse.protobuf.protobuf.Enum;
 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.ScalarType;
 import com.google.eclipse.protobuf.protobuf.ScalarTypeLink;
 import com.google.eclipse.protobuf.protobuf.TypeLink;
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
index b71ed89..e9d8556 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
@@ -28,6 +28,8 @@
 import com.google.eclipse.protobuf.protobuf.GroupElement;
 import com.google.eclipse.protobuf.protobuf.IndexedElement;
 import com.google.eclipse.protobuf.protobuf.LiteralLink;
+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.MessageLink;
@@ -84,6 +86,12 @@
   @SuppressWarnings("unused")
   public IScope scope_ComplexTypeLink_target(ComplexTypeLink link, EReference r) {
     EObject c = link.eContainer();
+    if (c instanceof MapType) {
+      c = c.eContainer();
+    }
+    if (c instanceof MapTypeLink) {
+      c = c.eContainer();
+    }
     if (c instanceof MessageField) {
       MessageField field = (MessageField) c;
       Collection<IEObjectDescription> complexTypes = potentialComplexTypesFor(field);
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.java
index 184ec86..8b6c645 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.java
@@ -27,6 +27,8 @@
   public static String fieldNumbersMustBePositive;
   public static String importingUnsupportedSyntax;
   public static String importNotFound;
+  public static String invalidMapKeyType;
+  public static String invalidMapValueType;
   public static String literalNotInEnum;
   public static String mapWithModifier;
   public static String missingFieldNumber;
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.properties b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.properties
index 1c061db..df4f28f 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.properties
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/Messages.properties
@@ -11,6 +11,8 @@
 fieldNumbersMustBePositive = Field numbers must be positive integers.
 importingUnsupportedSyntax = Importing unsupported file (directly or indirectly.) This may cause errors related to unresolved references.
 importNotFound = Import \"%s\" was not found.
+invalidMapKeyType = Key in a map field cannot be of float, double, bytes or message type.
+invalidMapValueType = Value in a map field cannot be a map.
 literalNotInEnum = Enum type \"%s\" has no value named \"%s\".
 mapWithModifier = Field labels (required/optional/repeated) are not allowed on map fields.
 missingFieldNumber = Missing field number.
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator.java
index 3a83d80..adbb50d 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/validation/ProtobufJavaValidator.java
@@ -8,6 +8,8 @@
  */
 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__NAME;
 import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.SYNTAX__NAME;
@@ -15,6 +17,8 @@
 import static com.google.eclipse.protobuf.validation.Messages.expectedSyntaxIdentifier;
 import static com.google.eclipse.protobuf.validation.Messages.fieldNumberAlreadyUsed;
 import static com.google.eclipse.protobuf.validation.Messages.fieldNumbersMustBePositive;
+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.missingModifier;
 import static com.google.eclipse.protobuf.validation.Messages.multiplePackages;
@@ -28,6 +32,7 @@
 import com.google.eclipse.protobuf.model.util.Protobufs;
 import com.google.eclipse.protobuf.naming.NameResolver;
 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.MessageElement;
@@ -37,7 +42,10 @@
 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.ScalarType;
+import com.google.eclipse.protobuf.protobuf.ScalarTypeLink;
 import com.google.eclipse.protobuf.protobuf.Syntax;
+import com.google.eclipse.protobuf.protobuf.TypeLink;
 import com.google.inject.Inject;
 
 import org.eclipse.emf.ecore.EObject;
@@ -57,6 +65,8 @@
   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";
 
   @Inject private IndexedElements indexedElements;
   @Inject private NameResolver nameResolver;
@@ -197,4 +207,25 @@
   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;
+    }
+  }
 }