Fix proto3 repeated field encoding without packed option (#635)

proto3 repeated fields are packed by default, see
https://developers.google.com/protocol-buffers/docs/encoding#packed.

Fixes #345
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index 651302a..3a7ada9 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -56,7 +56,9 @@
 	package1 \
 	package2 \
 	package3 \
+	proto2_repeated \
 	proto3_optional \
+	proto3_repeated \
 	service \
 	service2 \
 	service3 \
diff --git a/protoc_plugin/lib/src/extension_generator.dart b/protoc_plugin/lib/src/extension_generator.dart
index 295100f..4404e07 100644
--- a/protoc_plugin/lib/src/extension_generator.dart
+++ b/protoc_plugin/lib/src/extension_generator.dart
@@ -122,7 +122,7 @@
       }
     } else {
       invocation = '$protobufImportPrefix.Extension<$dartType>';
-      named['defaultOrMaker'] = _field.generateDefaultFunction(fileGen!);
+      named['defaultOrMaker'] = _field.generateDefaultFunction();
       if (type.isMessage || type.isGroup) {
         named['subBuilder'] = '$dartType.create';
       } else if (type.isEnum) {
diff --git a/protoc_plugin/lib/src/file_generator.dart b/protoc_plugin/lib/src/file_generator.dart
index ebe141e..b2600b6 100644
--- a/protoc_plugin/lib/src/file_generator.dart
+++ b/protoc_plugin/lib/src/file_generator.dart
@@ -30,6 +30,11 @@
       ': $value';
 }
 
+enum ProtoSyntax {
+  proto2,
+  proto3,
+}
+
 /// Generates the Dart output files for one .proto input file.
 ///
 /// Outputs include .pb.dart, pbenum.dart, and .pbjson.dart.
@@ -143,8 +148,13 @@
   /// True if cross-references have been resolved.
   bool _linked = false;
 
+  final ProtoSyntax syntax;
+
   FileGenerator(this.descriptor, this.options)
-      : protoFileUri = Uri.file(descriptor.name) {
+      : protoFileUri = Uri.file(descriptor.name),
+        syntax = descriptor.syntax == 'proto3'
+            ? ProtoSyntax.proto3
+            : ProtoSyntax.proto2 {
     if (protoFileUri.isAbsolute) {
       // protoc should never generate an import with an absolute path.
       throw 'FAILURE: Import with absolute path is not supported';
diff --git a/protoc_plugin/lib/src/message_generator.dart b/protoc_plugin/lib/src/message_generator.dart
index 9b8c560..a2efc02 100644
--- a/protoc_plugin/lib/src/message_generator.dart
+++ b/protoc_plugin/lib/src/message_generator.dart
@@ -342,7 +342,7 @@
         }
 
         for (var field in _fieldList) {
-          out.println(field.generateBuilderInfoCall(fileGen, package));
+          out.println(field.generateBuilderInfoCall(package));
         }
 
         if (_descriptor.extensionRange.isNotEmpty) {
@@ -372,7 +372,7 @@
                 '  ${field.baseType.getRepeatedDartTypeIterable(fileGen)}? ${field.memberNames!.fieldName},');
           } else {
             out.println(
-                '  ${field.getDartType(fileGen)}? ${field.memberNames!.fieldName},');
+                '  ${field.getDartType()}? ${field.memberNames!.fieldName},');
           }
         }
         out.print('}');
@@ -510,7 +510,7 @@
 
   void generateFieldAccessorsMutators(
       ProtobufField field, IndentingWriter out, List<int> memberFieldPath) {
-    var fieldTypeString = field.getDartType(fileGen);
+    var fieldTypeString = field.getDartType();
     var defaultExpr = field.getDefaultExpr();
     var names = field.memberNames;
 
diff --git a/protoc_plugin/lib/src/protobuf_field.dart b/protoc_plugin/lib/src/protobuf_field.dart
index 5fa3b1c..b8f5b3f 100644
--- a/protoc_plugin/lib/src/protobuf_field.dart
+++ b/protoc_plugin/lib/src/protobuf_field.dart
@@ -24,6 +24,7 @@
 
   final String fullName;
   final BaseType baseType;
+  final ProtobufContainer parent;
 
   ProtobufField.message(
       FieldNames names, ProtobufContainer parent, GenerationContext ctx)
@@ -33,8 +34,8 @@
       ProtobufContainer parent, GenerationContext ctx)
       : this._(descriptor, null, parent, ctx);
 
-  ProtobufField._(this.descriptor, FieldNames? dartNames,
-      ProtobufContainer parent, GenerationContext ctx)
+  ProtobufField._(this.descriptor, FieldNames? dartNames, this.parent,
+      GenerationContext ctx)
       : memberNames = dartNames,
         fullName = '${parent.fullName}.${descriptor.name}',
         baseType = BaseType(descriptor, ctx);
@@ -61,8 +62,33 @@
   bool get isRepeated =>
       descriptor.label == FieldDescriptorProto_Label.LABEL_REPEATED;
 
-  /// True if the field is to be encoded with [packed=true] encoding.
-  bool get isPacked => isRepeated && descriptor.options.packed;
+  /// True if a numeric field is repeated and it should be encoded with packed
+  /// encoding. In proto3 repeated fields are encoded as packed by default.
+  /// proto2 requires `[packed=true]` option.
+  bool get isPacked {
+    if (!isRepeated) {
+      return false; // only repeated fields can be packed
+    }
+
+    if (!baseType.isPackable) {
+      return false;
+    }
+
+    switch (parent.fileGen!.syntax) {
+      case ProtoSyntax.proto3:
+        if (!descriptor.hasOptions()) {
+          return true; // packed by default in proto3
+        } else {
+          return !descriptor.options.hasPacked() || descriptor.options.packed;
+        }
+      case ProtoSyntax.proto2:
+        if (!descriptor.hasOptions()) {
+          return false; // not packed by default in proto3
+        } else {
+          return descriptor.options.packed;
+        }
+    }
+  }
 
   /// Whether the field has the `overrideGetter` annotation set to true.
   bool get overridesGetter => _hasBooleanOption(Dart_options.overrideGetter);
@@ -111,16 +137,15 @@
   /// Returns the expression to use for the Dart type.
   ///
   /// This will be a List for repeated types.
-  /// [fileGen] represents the .proto file where we are generating code.
-  String getDartType(FileGenerator fileGen) {
+  String getDartType() {
     if (isMapField) {
       final d = baseType.generator as MessageGenerator;
-      var keyType = d._fieldList[0].baseType.getDartType(fileGen);
-      var valueType = d._fieldList[1].baseType.getDartType(fileGen);
+      var keyType = d._fieldList[0].baseType.getDartType(parent.fileGen!);
+      var valueType = d._fieldList[1].baseType.getDartType(parent.fileGen!);
       return '$coreImportPrefix.Map<$keyType, $valueType>';
     }
-    if (isRepeated) return baseType.getRepeatedDartType(fileGen);
-    return baseType.getDartType(fileGen);
+    if (isRepeated) return baseType.getRepeatedDartType(parent.fileGen!);
+    return baseType.getDartType(parent.fileGen!);
   }
 
   /// Returns the tag number of the underlying proto field.
@@ -162,15 +187,14 @@
 
   /// Returns Dart code adding this field to a BuilderInfo object.
   /// The call will start with ".." and a method name.
-  /// [fileGen] represents the .proto file where the code will be evaluated.
-  String generateBuilderInfoCall(FileGenerator fileGen, String package) {
+  String generateBuilderInfoCall(String package) {
     assert(descriptor.hasJsonName());
     var quotedName = configurationDependent(
       'protobuf.omit_field_names',
       quoted(descriptor.jsonName),
     );
 
-    var type = baseType.getDartType(fileGen);
+    var type = baseType.getDartType(parent.fileGen!);
 
     String invocation;
 
@@ -183,8 +207,8 @@
       final generator = baseType.generator as MessageGenerator;
       var key = generator._fieldList[0];
       var value = generator._fieldList[1];
-      var keyType = key.baseType.getDartType(fileGen);
-      var valueType = value.baseType.getDartType(fileGen);
+      var keyType = key.baseType.getDartType(parent.fileGen!);
+      var valueType = value.baseType.getDartType(parent.fileGen!);
 
       invocation = 'm<$keyType, $valueType>';
 
@@ -197,7 +221,7 @@
       if (value.baseType.isEnum) {
         named['valueOf'] = '$valueType.valueOf';
         named['enumValues'] = '$valueType.values';
-        named['defaultEnumValue'] = value.generateDefaultFunction(fileGen);
+        named['defaultEnumValue'] = value.generateDefaultFunction();
       }
       if (package != '') {
         named['packageName'] =
@@ -219,12 +243,12 @@
         } else if (baseType.isEnum) {
           named['valueOf'] = '$type.valueOf';
           named['enumValues'] = '$type.values';
-          named['defaultEnumValue'] = generateDefaultFunction(fileGen);
+          named['defaultEnumValue'] = generateDefaultFunction();
         }
       }
     } else {
       // Singular field.
-      var makeDefault = generateDefaultFunction(fileGen);
+      var makeDefault = generateDefaultFunction();
 
       if (baseType.isEnum) {
         args.add(typeConstant);
@@ -302,10 +326,7 @@
   }
 
   /// Returns a function expression that returns the field's default value.
-  ///
-  /// [fileGen] represents the .proto file where the expression will be
-  /// evaluated.
-  String? generateDefaultFunction(FileGenerator? fileGen) {
+  String? generateDefaultFunction() {
     assert(!isRepeated);
     switch (descriptor.type) {
       case FieldDescriptorProto_Type.TYPE_BOOL:
@@ -359,9 +380,9 @@
         return '() => <$coreImportPrefix.int>[$byteList]';
       case FieldDescriptorProto_Type.TYPE_GROUP:
       case FieldDescriptorProto_Type.TYPE_MESSAGE:
-        return '${baseType.getDartType(fileGen!)}.getDefault';
+        return '${baseType.getDartType(parent.fileGen!)}.getDefault';
       case FieldDescriptorProto_Type.TYPE_ENUM:
-        var className = baseType.getDartType(fileGen!);
+        var className = baseType.getDartType(parent.fileGen!);
         final gen = baseType.generator as EnumGenerator;
         if (descriptor.hasDefaultValue() &&
             descriptor.defaultValue.isNotEmpty) {
diff --git a/protoc_plugin/test/protos/proto2_repeated.proto b/protoc_plugin/test/protos/proto2_repeated.proto
new file mode 100644
index 0000000..ee3b156
--- /dev/null
+++ b/protoc_plugin/test/protos/proto2_repeated.proto
@@ -0,0 +1,11 @@
+// Copyright (c) 2017, 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 = "proto2";
+
+message Proto2Repeated {
+  repeated int32 ints_default = 1;
+  repeated int32 ints_packed = 2 [packed=true];
+  repeated int32 ints_not_packed = 3 [packed=false];
+}
diff --git a/protoc_plugin/test/protos/proto3_repeated.proto b/protoc_plugin/test/protos/proto3_repeated.proto
new file mode 100644
index 0000000..723d154
--- /dev/null
+++ b/protoc_plugin/test/protos/proto3_repeated.proto
@@ -0,0 +1,11 @@
+// Copyright (c) 2017, 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 Proto3Repeated {
+  repeated int32 ints_default = 1;
+  repeated int32 ints_packed = 2 [packed=true];
+  repeated int32 ints_not_packed = 3 [packed=false];
+}
diff --git a/protoc_plugin/test/repeated_encoding_test.dart b/protoc_plugin/test/repeated_encoding_test.dart
new file mode 100644
index 0000000..17a990a
--- /dev/null
+++ b/protoc_plugin/test/repeated_encoding_test.dart
@@ -0,0 +1,50 @@
+#!/usr/bin/env dart
+// Copyright (c) 2015, 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/proto2_repeated.pb.dart';
+import '../out/protos/proto3_repeated.pb.dart';
+
+void main() {
+  test('check proto2 and proto3 repeated field encodings', () {
+    final proto2 = Proto2Repeated(
+        intsDefault: [1, 2], intsPacked: [1, 2], intsNotPacked: [1, 2]);
+    final proto2Encoded = proto2.writeToBuffer();
+    expect(
+        proto2Encoded.toList(),
+        equals([
+          8, // field = 1, type = varint
+          1, // value = 1
+          8, // field = 1, type = varint
+          2, // value = 2
+          18, // field = 2, type = length delimited
+          2, // length = 2
+          1, 2, // values = [1, 2]
+          24, // field = 3, type = varint
+          1, // value = 1
+          24, // field = 3, type = varint
+          2, // value = 2
+        ]));
+
+    final proto3 = Proto3Repeated(
+        intsDefault: [1, 2], intsPacked: [1, 2], intsNotPacked: [1, 2]);
+    final proto3Encoded = proto3.writeToBuffer();
+    expect(
+        proto3Encoded.toList(),
+        equals([
+          10, // field = 1, type = length delimited
+          2, // length = 2
+          1, 2, // values = [1, 2]
+          18, // field = 2, type = length delimited
+          2, // length = 2
+          1, 2, // values = [1, 2]
+          24, // field = 3, type = varint
+          1, // value = 1
+          24, // field = 3, type = varint
+          2, // value = 2
+        ]));
+  });
+}