Emit binary coded descriptors from dart protoc_plugin. (#406)

These contain extensions to the descriptors, and thus can be used to
reflect over the options given to the descriptor
diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md
index 0405d5f..1e41a67 100644
--- a/protoc_plugin/CHANGELOG.md
+++ b/protoc_plugin/CHANGELOG.md
@@ -1,7 +1,13 @@
+## 19.3.1
+
+* Emit binary coded descriptors, which can be used to reflect over the options
+  given to the descriptor.
+
 ## 19.3.0
 
 * Generate constructors with optional named arguments for prefilling fields.
 * Output language version 2.7 in generated files to support extension methods.
+
 ## 19.2.1
 
 * Support optional proto3 fields.
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index 1aaec06..df2b114 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -25,6 +25,7 @@
 	google/protobuf/unittest_well_known_types \
 	google/protobuf/unittest \
 	google/protobuf/wrappers \
+	custom_option \
 	dart_name \
         default_value_escape \
 	enum_extension \
diff --git a/protoc_plugin/lib/code_generator.dart b/protoc_plugin/lib/code_generator.dart
index 753d3ac..2ae410c 100644
--- a/protoc_plugin/lib/code_generator.dart
+++ b/protoc_plugin/lib/code_generator.dart
@@ -28,6 +28,9 @@
 
   String get fileImportPrefix => _getFileImportPrefix();
 
+  String get binaryDescriptorName =>
+      '${lowerCaseFirstLetter(classname)}Descriptor';
+
   String _getFileImportPrefix() {
     var path = fileGen.protoFileUri.toString();
     if (importPrefixes.containsKey(path)) {
@@ -43,6 +46,21 @@
   ///
   /// (Represents the .pb.dart file that we need to import in order to use it.)
   FileGenerator get fileGen;
+
+  // The generator containing this entity.
+  ProtobufContainer get _parent;
+
+  /// The top-level parent of this entity. If this entity is a top-level entity,
+  /// returns this.
+  ProtobufContainer get toplevelParent {
+    if (_parent == null) {
+      return null;
+    }
+    if (_parent is FileGenerator) {
+      return this;
+    }
+    return _parent.toplevelParent;
+  }
 }
 
 class CodeGenerator extends ProtobufContainer {
@@ -112,5 +130,7 @@
   @override
   FileGenerator get fileGen => null;
   @override
+  ProtobufContainer get _parent => null;
+  @override
   List<int> get fieldPath => [];
 }
diff --git a/protoc_plugin/lib/enum_generator.dart b/protoc_plugin/lib/enum_generator.dart
index c27541a..e02ac7f 100644
--- a/protoc_plugin/lib/enum_generator.dart
+++ b/protoc_plugin/lib/enum_generator.dart
@@ -11,6 +11,7 @@
 }
 
 class EnumGenerator extends ProtobufContainer {
+  @override
   final ProtobufContainer _parent;
   @override
   final String classname;
@@ -170,6 +171,8 @@
     var name = getJsonConstant(fileGen);
     var json = _descriptor.writeToJsonMap();
 
+    out.println('@$_coreImportPrefix.Deprecated'
+        '(\'Use ${toplevelParent.binaryDescriptorName} instead\')');
     out.print("const $name = ");
     writeJsonConst(out, json);
     out.println(";");
diff --git a/protoc_plugin/lib/file_generator.dart b/protoc_plugin/lib/file_generator.dart
index f985c64..80150dc 100644
--- a/protoc_plugin/lib/file_generator.dart
+++ b/protoc_plugin/lib/file_generator.dart
@@ -9,13 +9,18 @@
 const String _protobufImportPrefix = r'$pb';
 const String _asyncImportPrefix = r'$async';
 const String _coreImportPrefix = r'$core';
+const String _convertImportPrefix = r'$convert';
 const String _fixnumImportPrefix = r'$fixnum';
 const String _grpcImportPrefix = r'$grpc';
 const String _mixinImportPrefix = r'$mixin';
+const String _typedDataImportPrefix = r'$typed_data';
 const String _protobufImport =
     "import 'package:protobuf/protobuf.dart' as $_protobufImportPrefix;";
 const String _asyncImport = "import 'dart:async' as $_asyncImportPrefix;";
 const String _coreImport = "import 'dart:core' as $_coreImportPrefix;";
+const String _typedDataImport =
+    "import 'dart:typed_data' as $_typedDataImportPrefix;";
+const String _convertImport = "import 'dart:convert' as $_convertImportPrefix;";
 const String _grpcImport =
     "import 'package:grpc/service_api.dart' as $_grpcImportPrefix;";
 
@@ -207,6 +212,8 @@
   @override
   FileGenerator get fileGen => this;
   @override
+  ProtobufContainer get _parent => null;
+  @override
   List<int> get fieldPath => [];
 
   /// Generates all the Dart files for this .proto file.
@@ -437,7 +444,8 @@
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
     if (!_linked) throw StateError("not linked");
     var out = makeWriter();
-    _writeHeading(out);
+    _writeHeading(out,
+        extraIgnores: {'deprecated_member_use_from_same_package'});
 
     if (serviceGenerators.isNotEmpty) {
       out.println(_asyncImport);
@@ -508,13 +516,27 @@
     return _formatter.format(out.toString());
   }
 
+  void writeBinaryDescriptor(IndentingWriter out, String identifierName,
+      String name, GeneratedMessage descriptor) {
+    var descriptorText = base64Encode(descriptor.writeToBuffer());
+    out.println('/// Descriptor for `$name`. Decode as a '
+        '`${descriptor.info_.qualifiedMessageName}`.');
+    out.println('final $_typedDataImportPrefix.Uint8List '
+        '$identifierName = '
+        '$_convertImportPrefix.base64Decode(\'$descriptorText\');');
+  }
+
   /// Returns the contents of the .pbjson.dart file for this .proto file.
   String generateJsonFile(
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
     if (!_linked) throw StateError("not linked");
     var out = makeWriter();
-    _writeHeading(out);
+    _writeHeading(out,
+        extraIgnores: {'deprecated_member_use_from_same_package'});
 
+    out.println(_coreImport);
+    out.println(_convertImport);
+    out.println(_typedDataImport);
     // Import the .pbjson.dart files we depend on.
     var imports = _findJsonProtosToImport();
     for (var target in imports) {
@@ -524,12 +546,18 @@
 
     for (var e in enumGenerators) {
       e.generateConstants(out);
+      writeBinaryDescriptor(
+          out, e.binaryDescriptorName, e._descriptor.name, e._descriptor);
     }
     for (var m in messageGenerators) {
       m.generateConstants(out);
+      writeBinaryDescriptor(
+          out, m.binaryDescriptorName, m._descriptor.name, m._descriptor);
     }
     for (var s in serviceGenerators) {
       s.generateConstants(out);
+      writeBinaryDescriptor(
+          out, s.binaryDescriptorName, s._descriptor.name, s._descriptor);
     }
 
     return out.toString();
@@ -553,14 +581,17 @@
   }
 
   /// Writes the header at the top of the dart file.
-  void _writeHeading(IndentingWriter out) {
+  void _writeHeading(IndentingWriter out,
+      {Set<String> extraIgnores = const <String>{}}) {
+    var extraIgnoresString =
+        extraIgnores.isEmpty ? '' : ',${extraIgnores.join(',')}';
     out.println('''
 ///
 //  Generated code. Do not modify.
 //  source: ${descriptor.name}
 //
 // @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields$extraIgnoresString
 ''');
   }
 
diff --git a/protoc_plugin/lib/grpc_generator.dart b/protoc_plugin/lib/grpc_generator.dart
index 1d67d72..71bebf1 100644
--- a/protoc_plugin/lib/grpc_generator.dart
+++ b/protoc_plugin/lib/grpc_generator.dart
@@ -181,8 +181,7 @@
   factory _GrpcMethod(GrpcServiceGenerator service, GenerationContext ctx,
       MethodDescriptorProto method) {
     final grpcName = method.name;
-    final dartName =
-        grpcName.substring(0, 1).toLowerCase() + grpcName.substring(1);
+    final dartName = lowerCaseFirstLetter(grpcName);
 
     final clientStreaming = method.clientStreaming;
     final serverStreaming = method.serverStreaming;
diff --git a/protoc_plugin/lib/message_generator.dart b/protoc_plugin/lib/message_generator.dart
index 79e46d1..dd0bc0c 100644
--- a/protoc_plugin/lib/message_generator.dart
+++ b/protoc_plugin/lib/message_generator.dart
@@ -51,6 +51,7 @@
 
   PbMixin mixin;
 
+  @override
   final ProtobufContainer _parent;
   final DescriptorProto _descriptor;
   final List<EnumGenerator> _enumGenerators = <EnumGenerator>[];
@@ -683,6 +684,8 @@
     var nestedEnumNames =
         _enumGenerators.map((e) => e.getJsonConstant(fileGen)).toList();
 
+    out.println('@$_coreImportPrefix.Deprecated'
+        '(\'Use ${toplevelParent.binaryDescriptorName} instead\')');
     out.addBlock("const $name = const {", "};", () {
       for (var key in json.keys) {
         out.print("'$key': ");
diff --git a/protoc_plugin/lib/names.dart b/protoc_plugin/lib/names.dart
index b93d79e..9e022ca 100644
--- a/protoc_plugin/lib/names.dart
+++ b/protoc_plugin/lib/names.dart
@@ -431,9 +431,8 @@
 
 /// The name to use by default for the Dart getter and setter.
 /// (A suffix will be added if there is a conflict.)
-String _defaultFieldName(String fieldMethodSuffix) {
-  return '${fieldMethodSuffix[0].toLowerCase()}${fieldMethodSuffix.substring(1)}';
-}
+String _defaultFieldName(String fieldMethodSuffix) =>
+    lowerCaseFirstLetter(fieldMethodSuffix);
 
 String _defaultHasMethodName(String fieldMethodSuffix) =>
     'has$fieldMethodSuffix';
@@ -583,3 +582,6 @@
   // The number of entries is one higher than the highest seen index.
   return highestIndexSeen + 1;
 }
+
+String lowerCaseFirstLetter(String input) =>
+    input[0].toLowerCase() + input.substring(1);
diff --git a/protoc_plugin/lib/protoc.dart b/protoc_plugin/lib/protoc.dart
index f836d94..8757465 100644
--- a/protoc_plugin/lib/protoc.dart
+++ b/protoc_plugin/lib/protoc.dart
@@ -1,5 +1,6 @@
 library protoc;
 
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:dart_style/dart_style.dart';
diff --git a/protoc_plugin/lib/service_generator.dart b/protoc_plugin/lib/service_generator.dart
index 31ed67c..af32009 100644
--- a/protoc_plugin/lib/service_generator.dart
+++ b/protoc_plugin/lib/service_generator.dart
@@ -131,8 +131,7 @@
 
   List<MethodDescriptorProto> get _methodDescriptors => _descriptor.method;
 
-  String _methodName(String name) =>
-      name.substring(0, 1).toLowerCase() + name.substring(1);
+  String _methodName(String name) => lowerCaseFirstLetter(name);
 
   String get _parentClass => _generatedService;
 
@@ -224,6 +223,9 @@
     for (var key in _transitiveDeps.keys) {
       typeConstants[key] = _transitiveDeps[key].getJsonConstant(fileGen);
     }
+
+    out.println('@$_coreImportPrefix.Deprecated'
+        '(\'Use ${binaryDescriptorName} instead\')');
     out.addBlock("const $messageJsonConstant = const {", "};", () {
       for (var key in typeConstants.keys) {
         var typeConst = typeConstants[key];
@@ -241,6 +243,14 @@
     }
   }
 
+  String get binaryDescriptorName {
+    var prefix = lowerCaseFirstLetter(classname);
+    if (prefix.endsWith('Base')) {
+      prefix = prefix.substring(0, prefix.length - 4);
+    }
+    return '${prefix}Descriptor';
+  }
+
   static final String _future = '$_asyncImportPrefix.Future';
   static final String _generatedMessage =
       '$_protobufImportPrefix.GeneratedMessage';
diff --git a/protoc_plugin/pubspec.yaml b/protoc_plugin/pubspec.yaml
index 49562ba..b3e5cb9 100644
--- a/protoc_plugin/pubspec.yaml
+++ b/protoc_plugin/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protoc_plugin
-version: 19.3.0
+version: 19.3.1
 description: Protoc compiler plugin to generate Dart code
 homepage: https://github.com/dart-lang/protobuf
 
diff --git a/protoc_plugin/test/all_tests.dart b/protoc_plugin/test/all_tests.dart
index fb96be2..fc3408f 100755
--- a/protoc_plugin/test/all_tests.dart
+++ b/protoc_plugin/test/all_tests.dart
@@ -10,6 +10,7 @@
 import 'client_generator_test.dart' as client_generator;
 import 'const_generator_test.dart' as const_generator;
 import 'default_value_escape_test.dart' as default_value_escape;
+import 'descriptor_test.dart' as descriptor_test;
 import 'enum_generator_test.dart' as enum_generator;
 import 'extension_generator_test.dart' as extension_generator_test;
 import 'extension_test.dart' as extension_test;
@@ -50,6 +51,7 @@
   client_generator.main();
   const_generator.main();
   default_value_escape.main();
+  descriptor_test.main();
   enum_generator.main();
   extension_generator_test.main();
   extension_test.main();
diff --git a/protoc_plugin/test/descriptor_test.dart b/protoc_plugin/test/descriptor_test.dart
new file mode 100644
index 0000000..b58353a
--- /dev/null
+++ b/protoc_plugin/test/descriptor_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, 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:protoc_plugin/src/descriptor.pb.dart';
+import 'package:protobuf/protobuf.dart';
+import '../out/protos/google/protobuf/unittest.pbjson.dart';
+import '../out/protos/custom_option.pb.dart';
+import '../out/protos/custom_option.pbjson.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('Can decode message descriptor', () {
+    final descriptor = DescriptorProto.fromBuffer(testAllTypesDescriptor);
+    expect(descriptor.name, 'TestAllTypes');
+    final nestedEnumDescriptor = descriptor.enumType.first;
+    expect(nestedEnumDescriptor.name, 'NestedEnum');
+  });
+  test('Can decode enum descriptor', () {
+    final descriptor = EnumDescriptorProto.fromBuffer(foreignEnumDescriptor);
+    expect(descriptor.name, 'ForeignEnum');
+    expect(descriptor.value.map((v) => v.name),
+        ['FOREIGN_FOO', 'FOREIGN_BAR', 'FOREIGN_BAZ']);
+  });
+  test('Can decode service descriptor', () {
+    final descriptor = ServiceDescriptorProto.fromBuffer(testServiceDescriptor);
+    expect(descriptor.name, 'TestService');
+    expect(descriptor.method.map((m) => m.name), ['Foo', 'Bar']);
+  });
+  test('Can read custom options', () {
+    final registry = ExtensionRegistry()..add(Custom_option.myOption);
+    final descriptor =
+        DescriptorProto.fromBuffer(myMessageDescriptor, registry);
+    final option = descriptor.options.getExtension(Custom_option.myOption);
+    expect(option, 'Hello world!');
+  });
+}
diff --git a/protoc_plugin/test/goldens/oneMessage.pbjson b/protoc_plugin/test/goldens/oneMessage.pbjson
index 0cac887..b26d869 100644
--- a/protoc_plugin/test/goldens/oneMessage.pbjson
+++ b/protoc_plugin/test/goldens/oneMessage.pbjson
@@ -3,8 +3,12 @@
 //  source: test
 //
 // @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
 
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use phoneNumberDescriptor instead')
 const PhoneNumber$json = const {
   '1': 'PhoneNumber',
   '2': const [
@@ -14,3 +18,5 @@
   ],
 };
 
+/// Descriptor for `PhoneNumber`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List phoneNumberDescriptor = $convert.base64Decode('CgtQaG9uZU51bWJlchIWCgZudW1iZXIYASACKAlSBm51bWJlchIUCgR0eXBlGAIgASgFMgBSBHR5cGUSFQoEbmFtZRgDIAEoCToBJFIEbmFtZQ==');
diff --git a/protoc_plugin/test/goldens/service.pbserver b/protoc_plugin/test/goldens/service.pbserver
index 66d6e79..dbc1c3b 100644
--- a/protoc_plugin/test/goldens/service.pbserver
+++ b/protoc_plugin/test/goldens/service.pbserver
@@ -3,7 +3,7 @@
 //  source: test
 //
 // @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
 
 import 'dart:async' as $async;
 
diff --git a/protoc_plugin/test/goldens/serviceGenerator.pb.json b/protoc_plugin/test/goldens/serviceGenerator.pb.json
index 7a33efe..30784bf 100644
--- a/protoc_plugin/test/goldens/serviceGenerator.pb.json
+++ b/protoc_plugin/test/goldens/serviceGenerator.pb.json
@@ -3,18 +3,27 @@
 //  source: testpkg.proto
 //
 // @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
 
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
 import 'foobar.pbjson.dart' as $1;
 
+@$core.Deprecated('Use someRequestDescriptor instead')
 const SomeRequest$json = const {
   '1': 'SomeRequest',
 };
 
+/// Descriptor for `SomeRequest`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List someRequestDescriptor = $convert.base64Decode('CgtTb21lUmVxdWVzdA==');
+@$core.Deprecated('Use someReplyDescriptor instead')
 const SomeReply$json = const {
   '1': 'SomeReply',
 };
 
+/// Descriptor for `SomeReply`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List someReplyDescriptor = $convert.base64Decode('CglTb21lUmVwbHk=');
 const TestServiceBase$json = const {
   '1': 'Test',
   '2': const [
@@ -23,6 +32,7 @@
   ],
 };
 
+@$core.Deprecated('Use testServiceDescriptor instead')
 const TestServiceBase$messageJson = const {
   '.testpkg.SomeRequest': SomeRequest$json,
   '.testpkg.SomeReply': SomeReply$json,
@@ -30,3 +40,5 @@
   '.foo.bar.AnotherReply': $1.AnotherReply$json,
 };
 
+/// Descriptor for `Test`. Decode as a `google.protobuf.ServiceDescriptorProto`.
+final $typed_data.Uint8List testServiceDescriptor = $convert.base64Decode('CgRUZXN0EjMKB0FNZXRob2QSFC50ZXN0cGtnLlNvbWVSZXF1ZXN0GhIudGVzdHBrZy5Tb21lUmVwbHkSPQoNQW5vdGhlck1ldGhvZBIVLmZvby5iYXIuRW1wdHlNZXNzYWdlGhUuZm9vLmJhci5Bbm90aGVyUmVwbHk=');
diff --git a/protoc_plugin/test/goldens/topLevelEnum.pbjson b/protoc_plugin/test/goldens/topLevelEnum.pbjson
index 6a549fb..03bbcd9 100644
--- a/protoc_plugin/test/goldens/topLevelEnum.pbjson
+++ b/protoc_plugin/test/goldens/topLevelEnum.pbjson
@@ -3,8 +3,12 @@
 //  source: test
 //
 // @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
 
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use phoneTypeDescriptor instead')
 const PhoneType$json = const {
   '1': 'PhoneType',
   '2': const [
@@ -15,3 +19,5 @@
   ],
 };
 
+/// Descriptor for `PhoneType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List phoneTypeDescriptor = $convert.base64Decode('CglQaG9uZVR5cGUSCgoGTU9CSUxFEAASCAoESE9NRRABEggKBFdPUksQAhIMCghCVVNJTkVTUxAC');
diff --git a/protoc_plugin/test/message_test.dart b/protoc_plugin/test/message_test.dart
index 0a43062..555b83c 100755
--- a/protoc_plugin/test/message_test.dart
+++ b/protoc_plugin/test/message_test.dart
@@ -3,6 +3,8 @@
 // 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.
 
+// ignore_for_file: deprecated_member_use_from_same_package
+
 library message_test;
 
 import 'package:protoc_plugin/src/descriptor.pb.dart' show DescriptorProto;
diff --git a/protoc_plugin/test/protos/custom_option.proto b/protoc_plugin/test/protos/custom_option.proto
new file mode 100644
index 0000000..05d39bd
--- /dev/null
+++ b/protoc_plugin/test/protos/custom_option.proto
@@ -0,0 +1,15 @@
+// Copyright (c) 2020, 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";
+
+import "descriptor.proto";
+
+extend google.protobuf.MessageOptions {
+  optional string my_option = 51234;
+}
+
+message MyMessage {
+  option (my_option) = "Hello world!";
+}