Repository: google/protobuf.dart

Issue:
`PbList.toFrozenPbList` does not freeze the list in-place, allows modifying frozen messages
Discovered this while reading the code.

```proto
syntax = "proto3";

message Test {
  repeated int32 ints = 1;
}
```

```dart
import '../test.pb.dart';

void main() {
  Test test1 = Test.create();
  test1.ints.add(123);

  var ints = test1.ints;

  test1.freeze();
  ints.add(456);

  print(test1);
}
```

Output:

```
ints: 123
ints: 456
```

Expected output: it should fail with an exception.

`PbList` should implement freezing the same way `PbMap` implements it: set a bool flag in `PbList`, mark elements of the list as frozen.



Fix patch:
diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md
index f3ae34368..9b53c709c 100644
--- a/protobuf/CHANGELOG.md
+++ b/protobuf/CHANGELOG.md
@@ -2,11 +2,16 @@
 
 * `ReadonlyMessageMixin` removed ([#183], [#644])
 * `frozenMessageModificationHandler` removed ([#175], [#643])
+* `PbListBase` and `FrozenPbList` types are removed. All proto repeated fields
+  now use `PbList`. To check if a list is frozen, use `isFrozen` getter.
+  ([#624], [#626])
 
 [#183]: https://github.com/google/protobuf.dart/issues/183
 [#644]: https://github.com/google/protobuf.dart/pull/644
 [#175]: https://github.com/google/protobuf.dart/issues/175
 [#643]: https://github.com/google/protobuf.dart/pull/643
+[#624]: https://github.com/google/protobuf.dart/issues/624
+[#626]: https://github.com/google/protobuf.dart/pull/626
 
 ## 2.1.0
 
diff --git a/protobuf/lib/src/protobuf/extension_field_set.dart b/protobuf/lib/src/protobuf/extension_field_set.dart
index 4e04df595..93d291904 100644
--- a/protobuf/lib/src/protobuf/extension_field_set.dart
+++ b/protobuf/lib/src/protobuf/extension_field_set.dart
@@ -164,7 +164,7 @@ class _ExtensionFieldSet {
       final value = original._getFieldOrNull(extension);
       if (value == null) continue;
       if (extension.isRepeated) {
-        assert(value is PbListBase);
+        assert(value is PbList);
         _ensureRepeatedField(extension).addAll(value);
       } else {
         _setFieldUnchecked(extension, value);
@@ -179,12 +179,7 @@ class _ExtensionFieldSet {
       if (field.isRepeated) {
         final entries = _values[field.tagNumber];
         if (entries == null) continue;
-        if (field.isGroupOrMessage) {
-          for (var subMessage in entries as List<GeneratedMessage>) {
-            subMessage.freeze();
-          }
-        }
-        _values[field.tagNumber] = entries.toFrozenPbList();
+        entries.freeze();
       } else if (field.isGroupOrMessage) {
         final entry = _values[field.tagNumber];
         if (entry != null) {
diff --git a/protobuf/lib/src/protobuf/field_info.dart b/protobuf/lib/src/protobuf/field_info.dart
index 1bd3efe54..6bd6f2941 100644
--- a/protobuf/lib/src/protobuf/field_info.dart
+++ b/protobuf/lib/src/protobuf/field_info.dart
@@ -6,7 +6,10 @@ part of protobuf;
 
 /// An object representing a protobuf message field.
 class FieldInfo<T> {
-  FrozenPbList<T>? _emptyList;
+  /// Cached read-only empty list for this field type. For non-repeated fields
+  /// this is always `null`. Otherwise it starts as `null` and gets initialized
+  /// in `readonlyDefault`.
+  PbList<T>? _emptyList;
 
   /// Name of this field as the `json_name` reported by protoc.
   ///
@@ -156,7 +159,7 @@ class FieldInfo<T> {
   /// [GeneratedMessage.getField], doesn't create a repeated field.
   dynamic get readonlyDefault {
     if (isRepeated) {
-      return _emptyList ??= FrozenPbList._([]);
+      return _emptyList ??= PbList.unmodifiable();
     }
     return makeDefault!();
   }
@@ -276,8 +279,7 @@ class MapFieldInfo<K, V> extends FieldInfo<PbMap<K, V>?> {
       {ProtobufEnum? defaultEnumValue,
       String? protoName})
       : super(name, tagNumber, index, type,
-            defaultOrMaker: () =>
-                PbMap<K, V>(keyFieldType, valueFieldType, mapEntryBuilderInfo),
+            defaultOrMaker: () => PbMap<K, V>(keyFieldType, valueFieldType),
             defaultEnumValue: defaultEnumValue,
             protoName: protoName) {
     ArgumentError.checkNotNull(name, 'name');
diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart
index fdbb4692d..290a1e51e 100644
--- a/protobuf/lib/src/protobuf/field_set.dart
+++ b/protobuf/lib/src/protobuf/field_set.dart
@@ -147,18 +147,13 @@ class _FieldSet {
     _frozenState = true;
     for (var field in _meta.sortedByTag) {
       if (field.isRepeated) {
-        final entries = _values[field.index!];
-        if (entries == null) continue;
-        if (field.isGroupOrMessage) {
-          for (var subMessage in entries as List<GeneratedMessage>) {
-            subMessage.freeze();
-          }
-        }
-        _values[field.index!] = entries.toFrozenPbList();
+        PbList? list = _values[field.index!];
+        if (list == null) continue;
+        list.freeze();
       } else if (field.isMapField) {
         PbMap? map = _values[field.index!];
         if (map == null) continue;
-        _values[field.index!] = map.freeze();
+        map.freeze();
       } else if (field.isGroupOrMessage) {
         final entry = _values[field.index!];
         if (entry != null) {
@@ -699,7 +694,7 @@ class _FieldSet {
 
     void writeFieldValue(fieldValue, String name) {
       if (fieldValue == null) return;
-      if (fieldValue is PbListBase) {
+      if (fieldValue is PbList) {
         for (var value in fieldValue) {
           renderValue(name, value);
         }
@@ -791,14 +786,14 @@ class _FieldSet {
     if (fi.isRepeated) {
       if (mustClone) {
         // fieldValue must be a PbListBase of GeneratedMessage.
-        PbListBase<GeneratedMessage> pbList = fieldValue;
+        PbList<GeneratedMessage> pbList = fieldValue;
         var repeatedFields = fi._ensureRepeatedField(meta, this);
         for (var i = 0; i < pbList.length; ++i) {
           repeatedFields.add(pbList[i].deepCopy());
         }
       } else {
         // fieldValue must be at least a PbListBase.
-        PbListBase pbList = fieldValue;
+        PbList pbList = fieldValue;
         fi._ensureRepeatedField(meta, this).addAll(pbList);
       }
       return;
@@ -886,7 +881,7 @@ class _FieldSet {
             ..addAll(map);
         }
       } else if (fieldInfo.isRepeated) {
-        PbListBase? list = _values[index];
+        PbList? list = _values[index];
         if (list != null) {
           _values[index] = fieldInfo._createRepeatedField(_message!)
             ..addAll(list);
diff --git a/protobuf/lib/src/protobuf/pb_list.dart b/protobuf/lib/src/protobuf/pb_list.dart
index 6b53d1fbf..83fea6a31 100644
--- a/protobuf/lib/src/protobuf/pb_list.dart
+++ b/protobuf/lib/src/protobuf/pb_list.dart
@@ -6,84 +6,38 @@ part of protobuf;
 
 typedef CheckFunc<E> = void Function(E? x);
 
-class FrozenPbList<E> extends PbListBase<E> {
-  FrozenPbList._(List<E> wrappedList) : super._(wrappedList);
+class PbList<E> extends ListBase<E> {
+  final List<E> _wrappedList;
+  final CheckFunc<E> _check;
 
-  factory FrozenPbList.from(PbList<E> other) =>
-      FrozenPbList._(other._wrappedList);
+  bool _isReadOnly = false;
 
-  UnsupportedError _unsupported(String method) =>
-      UnsupportedError('Cannot call $method on an unmodifiable list');
+  bool get isFrozen => _isReadOnly;
 
-  @override
-  void operator []=(int index, E value) => throw _unsupported('set');
-  @override
-  set length(int newLength) => throw _unsupported('set length');
-  @override
-  void setAll(int index, Iterable<E> iterable) => throw _unsupported('setAll');
-  @override
-  void add(E? element) => throw _unsupported('add');
-  @override
-  void addAll(Iterable<E> iterable) => throw _unsupported('addAll');
-  @override
-  void insert(int index, E element) => throw _unsupported('insert');
-  @override
-  void insertAll(int index, Iterable<E> iterable) =>
-      throw _unsupported('insertAll');
-  @override
-  bool remove(Object? element) => throw _unsupported('remove');
-  @override
-  void removeWhere(bool Function(E element) test) =>
-      throw _unsupported('removeWhere');
-  @override
-  void retainWhere(bool Function(E element) test) =>
-      throw _unsupported('retainWhere');
-  @override
-  void sort([Comparator<E>? compare]) => throw _unsupported('sort');
-  @override
-  void shuffle([math.Random? random]) => throw _unsupported('shuffle');
-  @override
-  void clear() => throw _unsupported('clear');
-  @override
-  E removeAt(int index) => throw _unsupported('removeAt');
-  @override
-  E removeLast() => throw _unsupported('removeLast');
-  @override
-  void setRange(int start, int end, Iterable<E> iterable,
-          [int skipCount = 0]) =>
-      throw _unsupported('setRange');
-  @override
-  void removeRange(int start, int end) => throw _unsupported('removeRange');
-  @override
-  void replaceRange(int start, int end, Iterable<E> newContents) =>
-      throw _unsupported('replaceRange');
-  @override
-  void fillRange(int start, int end, [E? fill]) =>
-      throw _unsupported('fillRange');
-}
-
-class PbList<E> extends PbListBase<E> {
-  PbList({CheckFunc<E> check = _checkNotNull}) : super._noList(check: check);
+  PbList({check = _checkNotNull})
+      : _wrappedList = <E>[],
+        _check = check;
 
-  PbList.from(List from) : super._from(from);
+  PbList.unmodifiable()
+      : _wrappedList = const [],
+        _check = _checkNotNull,
+        _isReadOnly = true;
 
-  @Deprecated('Instead use the default constructor with a check function.'
-      'This constructor will be removed in the next major version.')
-  PbList.forFieldType(int fieldType)
-      : super._noList(check: getCheckFunction(fieldType));
-
-  /// Freezes the list by converting to [FrozenPbList].
-  FrozenPbList<E> toFrozenPbList() => FrozenPbList<E>.from(this);
+  PbList.from(List from)
+      : _wrappedList = List<E>.from(from),
+        _check = _checkNotNull;
 
   @override
   void add(E element) {
-    check(element);
+    _checkModifiable('add');
+    _check(element);
     _wrappedList.add(element);
   }
 
   @override
   void addAll(Iterable<E> iterable) {
-    iterable.forEach(check);
+    _checkModifiable('addAll');
+    iterable.forEach(_check);
     _wrappedList.addAll(iterable);
   }
 
@@ -91,225 +45,155 @@ class PbList<E> extends PbListBase<E> {
   Iterable<E> get reversed => _wrappedList.reversed;
 
   @override
-  void sort([int Function(E a, E b)? compare]) => _wrappedList.sort(compare);
+  void sort([int Function(E a, E b)? compare]) {
+    _checkModifiable('sort');
+    _wrappedList.sort(compare);
+  }
 
   @override
-  void shuffle([math.Random? random]) => _wrappedList.shuffle(random);
+  void shuffle([math.Random? random]) {
+    _checkModifiable('shuffle');
+    _wrappedList.shuffle(random);
+  }
 
   @override
-  void clear() => _wrappedList.clear();
+  void clear() {
+    _checkModifiable('clear');
+    _wrappedList.clear();
+  }
 
   @override
   void insert(int index, E element) {
-    check(element);
+    _checkModifiable('insert');
+    _check(element);
     _wrappedList.insert(index, element);
   }
 
   @override
   void insertAll(int index, Iterable<E> iterable) {
-    iterable.forEach(check);
+    _checkModifiable('insertAll');
+    iterable.forEach(_check);
     _wrappedList.insertAll(index, iterable);
   }
 
   @override
   void setAll(int index, Iterable<E> iterable) {
-    iterable.forEach(check);
+    _checkModifiable('setAll');
+    iterable.forEach(_check);
     _wrappedList.setAll(index, iterable);
   }
 
   @override
-  bool remove(Object? element) => _wrappedList.remove(element);
+  bool remove(Object? element) {
+    _checkModifiable('remove');
+    return _wrappedList.remove(element);
+  }
 
   @override
-  E removeAt(int index) => _wrappedList.removeAt(index);
+  E removeAt(int index) {
+    _checkModifiable('removeAt');
+    return _wrappedList.removeAt(index);
+  }
 
   @override
-  E removeLast() => _wrappedList.removeLast();
+  E removeLast() {
+    _checkModifiable('removeLast');
+    return _wrappedList.removeLast();
+  }
 
   @override
-  void removeWhere(bool Function(E element) test) =>
-      _wrappedList.removeWhere(test);
+  void removeWhere(bool Function(E element) test) {
+    _checkModifiable('removeWhere');
+    return _wrappedList.removeWhere(test);
+  }
 
   @override
-  void retainWhere(bool Function(E element) test) =>
-      _wrappedList.retainWhere(test);
+  void retainWhere(bool Function(E element) test) {
+    _checkModifiable('retainWhere');
+    return _wrappedList.retainWhere(test);
+  }
 
   @override
   void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) {
+    _checkModifiable('setRange');
     // NOTE: In case `take()` returns less than `end - start` elements, the
     // _wrappedList will fail with a `StateError`.
-    iterable.skip(skipCount).take(end - start).forEach(check);
+    iterable.skip(skipCount).take(end - start).forEach(_check);
     _wrappedList.setRange(start, end, iterable, skipCount);
   }
 
   @override
-  void removeRange(int start, int end) => _wrappedList.removeRange(start, end);
+  void removeRange(int start, int end) {
+    _checkModifiable('removeRange');
+    _wrappedList.removeRange(start, end);
+  }
 
   @override
   void fillRange(int start, int end, [E? fill]) {
-    check(fill);
+    _checkModifiable('fillRange');
+    _check(fill);
     _wrappedList.fillRange(start, end, fill);
   }
 
   @override
   void replaceRange(int start, int end, Iterable<E> newContents) {
+    _checkModifiable('replaceRange');
     final values = newContents.toList();
-    newContents.forEach(check);
+    newContents.forEach(_check);
     _wrappedList.replaceRange(start, end, values);
   }
-}
-
-abstract class PbListBase<E> extends ListBase<E> {
-  final List<E> _wrappedList;
-  final CheckFunc<E> check;
-
-  PbListBase._(this._wrappedList, {this.check = _checkNotNull});
-
-  PbListBase._noList({this.check = _checkNotNull}) : _wrappedList = <E>[] {
-    ArgumentError.checkNotNull(check, 'check');
-  }
-
-  PbListBase._from(List from)
-      // TODO(sra): Should this be validated?
-      : _wrappedList = List<E>.from(from),
-        check = _checkNotNull;
-
-  @override
-  bool operator ==(other) =>
-      (other is PbListBase) && _areListsEqual(other, this);
-
-  @override
-  int get hashCode => _HashUtils._hashObjects(_wrappedList);
 
   @override
-  Iterator<E> get iterator => _wrappedList.iterator;
-
-  @override
-  Iterable<T> map<T>(T Function(E e) f) => _wrappedList.map<T>(f);
-
-  @override
-  Iterable<E> where(bool Function(E element) test) => _wrappedList.where(test);
-
-  @override
-  Iterable<T> expand<T>(Iterable<T> Function(E element) f) =>
-      _wrappedList.expand(f);
-
-  @override
-  bool contains(Object? element) => _wrappedList.contains(element);
+  int get length => _wrappedList.length;
 
   @override
-  void forEach(void Function(E element) action) {
-    _wrappedList.forEach(action);
+  set length(int newLength) {
+    _checkModifiable('set length');
+    if (newLength > length) {
+      throw UnsupportedError('Extending protobuf lists is not supported');
+    }
+    _wrappedList.length = newLength;
   }
 
-  @override
-  E reduce(E Function(E value, E element) combine) =>
-      _wrappedList.reduce(combine);
-
-  @override
-  T fold<T>(T initialValue, T Function(T previousValue, E element) combine) =>
-      _wrappedList.fold(initialValue, combine);
-
-  @override
-  bool every(bool Function(E element) test) => _wrappedList.every(test);
-
-  @override
-  String join([String separator = '']) => _wrappedList.join(separator);
-
-  @override
-  bool any(bool Function(E element) test) => _wrappedList.any(test);
-
-  @override
-  List<E> toList({bool growable = true}) =>
-      _wrappedList.toList(growable: growable);
-
-  @override
-  Set<E> toSet() => _wrappedList.toSet();
-
-  @override
-  bool get isEmpty => _wrappedList.isEmpty;
-
-  @override
-  bool get isNotEmpty => _wrappedList.isNotEmpty;
-
-  @override
-  Iterable<E> take(int count) => _wrappedList.take(count);
-
-  @override
-  Iterable<E> takeWhile(bool Function(E value) test) =>
-      _wrappedList.takeWhile(test);
-
-  @override
-  Iterable<E> skip(int count) => _wrappedList.skip(count);
-
-  @override
-  Iterable<E> skipWhile(bool Function(E value) test) =>
-      _wrappedList.skipWhile(test);
-
-  @override
-  E get first => _wrappedList.first;
-
-  @override
-  E get last => _wrappedList.last;
-
-  @override
-  E get single => _wrappedList.single;
-
-  @override
-  E firstWhere(bool Function(E element) test, {E Function()? orElse}) =>
-      _wrappedList.firstWhere(test, orElse: orElse);
-
-  @override
-  E lastWhere(bool Function(E element) test, {E Function()? orElse}) =>
-      _wrappedList.lastWhere(test, orElse: orElse);
-
-  @override
-  E elementAt(int index) => _wrappedList.elementAt(index);
-
-  @override
-  String toString() => _wrappedList.toString();
-
   @override
   E operator [](int index) => _wrappedList[index];
 
   @override
-  int get length => _wrappedList.length;
-
-  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
-  @override
-  int indexOf(Object? element, [int start = 0]) =>
-      _wrappedList.indexOf(element as E, start);
+  void operator []=(int index, E value) {
+    _checkModifiable('set element');
+    _check(value);
+    _wrappedList[index] = value;
+  }
 
-  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
   @override
-  int lastIndexOf(Object? element, [int? start]) =>
-      _wrappedList.lastIndexOf(element as E, start);
+  bool operator ==(other) => other is PbList && _areListsEqual(other, this);
 
   @override
-  List<E> sublist(int start, [int? end]) => _wrappedList.sublist(start, end);
+  int get hashCode => _HashUtils._hashObjects(_wrappedList);
 
-  @override
-  Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end);
+  void freeze() {
+    if (_isReadOnly) {
+      return;
+    }
 
-  @override
-  Map<int, E> asMap() => _wrappedList.asMap();
+    _isReadOnly = true;
 
-  @override
-  void operator []=(int index, E value) {
-    check(value);
-    _wrappedList[index] = value;
+    // Per spec `repeated map<..>` and `repeated repeated ..` are not allowed
+    // so we only check for messages
+    if (_wrappedList.isNotEmpty && _wrappedList[0] is GeneratedMessage) {
+      for (var elem in _wrappedList as Iterable<GeneratedMessage>) {
+        elem.freeze();
+      }
+    }
   }
 
-  /// Unsupported -- violated non-null constraint imposed by protobufs.
-  ///
-  /// Changes the length of the list. If [newLength] is greater than the current
-  /// [length], entries are initialized to [:null:]. Throws an
-  /// [UnsupportedError] if the list is not extendable.
-  @override
-  set length(int newLength) {
-    if (newLength > length) {
-      throw UnsupportedError('Extending protobuf lists is not supported');
+  void _checkModifiable(String methodName) {
+    if (_isReadOnly) {
+      _readOnlyError(methodName);
     }
-    _wrappedList.length = newLength;
+  }
+
+  static Never _readOnlyError(String methodName) {
+    throw UnsupportedError("'$methodName' on a read-only list");
   }
 }
diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart
index 81d1f239e..052d10c4d 100644
--- a/protobuf/lib/src/protobuf/proto3_json.dart
+++ b/protobuf/lib/src/protobuf/proto3_json.dart
@@ -98,7 +98,7 @@ Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) {
             valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
       });
     } else if (fieldInfo.isRepeated) {
-      jsonValue = (value as PbListBase)
+      jsonValue = (value as PbList)
           .map((element) => valueToProto3Json(element, fieldInfo.type))
           .toList();
     } else {
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index 3a7ada9a7..939c4c02e 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -47,6 +47,7 @@ TEST_PROTO_LIST = \
 	mixins \
 	multiple_files_test \
 	nested_extension \
+	nested_message \
 	non_nested_extension \
 	oneof \
 	reserved_names \


Test patch:
diff --git a/protobuf/test/readonly_message_test.dart b/protobuf/test/readonly_message_test.dart
index 21b8ad0fb..4e379a6b4 100644
--- a/protobuf/test/readonly_message_test.dart
+++ b/protobuf/test/readonly_message_test.dart
@@ -90,24 +90,20 @@ void main() {
     var r = Rec.create()
       ..ints.add(10)
       ..freeze();
-    expect(
-        () => r.ints.clear(),
-        throwsError(UnsupportedError,
-            equals('Cannot call clear on an unmodifiable list')));
+    expect(() => r.ints.clear(),
+        throwsError(UnsupportedError, equals("'clear' on a read-only list")));
     expect(
         () => r.ints[0] = 2,
-        throwsError(UnsupportedError,
-            equals('Cannot call set on an unmodifiable list')));
+        throwsError(
+            UnsupportedError, equals("'set element' on a read-only list")));
     expect(() => r.sub.add(Rec.create()),
         throwsError(UnsupportedError, contains('add')));
 
     r = Rec.create()
       ..sub.add(Rec.create())
       ..freeze();
-    expect(
-        () => r.sub.add(Rec.create()),
-        throwsError(UnsupportedError,
-            equals('Cannot call add on an unmodifiable list')));
+    expect(() => r.sub.add(Rec.create()),
+        throwsError(UnsupportedError, equals("'add' on a read-only list")));
     expect(() => r.ints.length = 20,
         throwsError(UnsupportedError, contains('length')));
   });
diff --git a/protoc_plugin/test/freeze_test.dart b/protoc_plugin/test/freeze_test.dart
new file mode 100644
index 000000000..340057093
--- /dev/null
+++ b/protoc_plugin/test/freeze_test.dart
@@ -0,0 +1,57 @@
+#!/usr/bin/env dart
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import '../out/protos/nested_message.pb.dart';
+
+void main() {
+  test('testFreezingNestedFields', () {
+    Top top = Top(
+      nestedMessageList: [Nested(a: 1)],
+      nestedMessageMap: {1: Nested(a: 2)},
+      nestedMessage: Nested(a: 3),
+    );
+
+    // Create aliases to lists, maps, nested messages
+    var list = top.nestedMessageList;
+    var map = top.nestedMessageMap;
+    var msg1 = top.nestedMessageList[0];
+    var msg2 = top.nestedMessageMap[1]!;
+    var msg3 = top.nestedMessage;
+
+    top.freeze();
+
+    expect(top.isFrozen, true);
+
+    // Check list field
+    expect(top.nestedMessageList.length, 1);
+    expect(top.nestedMessageList[0].isFrozen, true);
+    expect(() => top.nestedMessageList.add(Nested(a: 0)),
+        throwsA(const TypeMatcher<UnsupportedError>()));
+
+    // Check map field
+    expect(top.nestedMessageMap.length, 1);
+    expect(top.nestedMessageMap[1]!.isFrozen, true);
+    expect(() => top.nestedMessageMap[2] = Nested(a: 0),
+        throwsA(const TypeMatcher<UnsupportedError>()));
+    expect(() => map[0] = Nested(a: 0),
+        throwsA(const TypeMatcher<UnsupportedError>()));
+
+    // Check message field
+    expect(top.nestedMessage.isFrozen, true);
+
+    // Check aliases
+    expect(() => list.add(Nested(a: 0)),
+        throwsA(const TypeMatcher<UnsupportedError>()));
+    expect(() => map[123] = Nested(a: 0),
+        throwsA(const TypeMatcher<UnsupportedError>()));
+    expect(list[0].isFrozen, true);
+    expect(map[1]!.isFrozen, true);
+    expect(msg1.isFrozen, true);
+    expect(msg2.isFrozen, true);
+    expect(msg3.isFrozen, true);
+  });
+}
diff --git a/protoc_plugin/test/protos/nested_message.proto b/protoc_plugin/test/protos/nested_message.proto
new file mode 100644
index 000000000..fd3515407
--- /dev/null
+++ b/protoc_plugin/test/protos/nested_message.proto
@@ -0,0 +1,15 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+syntax = "proto3";
+
+message Top {
+  repeated Nested nested_message_list = 1;
+  map<int32, Nested> nested_message_map = 2;
+  Nested nested_message = 3;
+}
+
+message Nested {
+  int32 a = 1;
+}