Treat empty and uninitialized Maps the same in equality checks (#638)

Treat an empty Map the same as an uninitialized Map when performing
equality checks. This ensures that reading a map field does not alter
the hashCode or equality comparison of the message itself.

Co-authored-by: Alex Greaves <[email protected]>
diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart
index 5044e86..89bb811 100644
--- a/protobuf/lib/src/protobuf/field_set.dart
+++ b/protobuf/lib/src/protobuf/field_set.dart
@@ -636,6 +636,11 @@
     // We don't want reading a field to change equality comparisons.
     if (val is List && val.isEmpty) return true;
 
+    // An empty map field is the same as uninitialized.
+    // This is because accessing a map field automatically creates it.
+    // We don't want reading a field to change equality comparisons.
+    if (val is Map && val.isEmpty) return true;
+
     // For now, initialized and uninitialized fields are different.
     // TODO(skybrian) consider other cases; should we compare with the
     // default value or not?
@@ -693,6 +698,10 @@
       return hash; // It's either repeated or an empty byte array.
     }
 
+    if (value is Map && value.isEmpty) {
+      return hash;
+    }
+
     hash = _HashUtils._combine(hash, fi.tagNumber);
     if (_isBytes(fi.type)) {
       // Bytes are represented as a List<int> (Usually with byte-data).
diff --git a/protoc_plugin/test/map_field_test.dart b/protoc_plugin/test/map_field_test.dart
index 3bd364f..eae4cfb 100644
--- a/protoc_plugin/test/map_field_test.dart
+++ b/protoc_plugin/test/map_field_test.dart
@@ -301,7 +301,7 @@
     expect(value is Map<int, List<int>>, true);
   });
 
-  test('named optional arguments in cosntructor', () {
+  test('named optional arguments in constructor', () {
     final testMap = TestMap(
       int32ToInt32Field: {1: 11, 2: 22, 3: 33},
       int32ToStringField: {1: '11', 2: '22', 3: '33'},
@@ -346,4 +346,14 @@
     expect(m.stringToInt32Field[''], 11);
     expect(m.stringToInt32Field['def'], 42);
   });
+
+  test('Map field reads should not affect equality or hash of message', () {
+    final m1 = TestMap.create();
+    final m2 = TestMap.create();
+    expect(m1, equals(m2));
+    expect(m1.hashCode, equals(m2.hashCode));
+    m1.int32ToStringField; // read a map field
+    expect(m1, equals(m2));
+    expect(m1.hashCode, equals(m2.hashCode));
+  });
 }