Forráskód Böngészése

Provide simple access to descriptor declarations in C#

This is primarily for access to comments, which would be expected to be available in a protoc plugin.

The implementation has two fiddly aspects:

- We use a Lazy<T> to avoid building the map before cross-linking. An alternative would be to crosslink at the end of the constructor, and remove the calls to CrossLink elsewhere. This would be generally better IMO, but deviate from the Java code.
- The casts to IReadOnlyList<DescriptorBase> are unfortunate. They'll always work, because these lists are always ReadOnlyCollection<T> for a descriptor type... but we can't use IList<DescriptorBase> as that's not covariant, and it's annoyingly fiddly to change the field to be of type ReadOnlyCollection<T>.
Jon Skeet 7 éve
szülő
commit
1711999078

+ 3 - 0
csharp/generate_protos.sh

@@ -43,6 +43,9 @@ $PROTOC -Isrc --csharp_out=csharp/src/Google.Protobuf \
 # Test protos
 $PROTOC -Isrc -Icsharp/protos \
     --csharp_out=csharp/src/Google.Protobuf.Test/TestProtos \
+    --descriptor_set_out=csharp/src/Google.Protobuf.Test/testprotos.pb \
+    --include_source_info \
+    --include_imports \
     csharp/protos/map_unittest_proto3.proto \
     csharp/protos/unittest_issues.proto \
     csharp/protos/unittest_custom_options_proto3.proto \

+ 30 - 0
csharp/protos/unittest_proto3.proto

@@ -368,7 +368,9 @@ message FooResponse {}
 message FooClientMessage {}
 message FooServerMessage{}
 
+// This is a test service
 service TestService {
+  // This is a test method
   rpc Foo(FooRequest) returns (FooResponse);
   rpc Bar(BarRequest) returns (BarResponse);
 }
@@ -378,3 +380,31 @@ message BarRequest  {}
 message BarResponse {}
 
 message TestEmptyMessage {}
+
+// This is leading detached comment 1
+
+// This is leading detached comment 2
+
+// This is a leading comment
+message CommentMessage {
+  // Leading nested message comment
+  message NestedCommentMessage {
+    // Leading nested message field comment
+    string nested_text = 1;
+  }
+  
+  // Leading nested enum comment
+  enum NestedCommentEnum {
+    // Zero value comment
+    ZERO_VALUE = 0;
+  }
+  
+  // Leading field comment
+  string text = 1; // Trailing field comment
+}
+
+// Leading enum comment
+enum CommentEnum {
+  // Zero value comment
+  ZERO_VALUE = 0;
+}

+ 4 - 0
csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj

@@ -27,4 +27,8 @@
     <TargetFrameworks>netcoreapp1.0</TargetFrameworks>
   </PropertyGroup>
 
+  <ItemGroup>
+    <EmbeddedResource Include="testprotos.pb" />
+  </ItemGroup>
+
 </Project>

+ 151 - 0
csharp/src/Google.Protobuf.Test/Reflection/DescriptorDeclarationTest.cs

@@ -0,0 +1,151 @@
+#region Copyright notice and license
+// Protocol Buffers - Google's data interchange format
+// Copyright 2018 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#endregion
+
+using Google.Protobuf.Reflection;
+using NUnit.Framework;
+using System.Linq;
+using System.Reflection;
+
+namespace Google.Protobuf.Test.Reflection
+{
+    // In reality this isn't a test for DescriptorDeclaration so much as the way they're loaded.
+    public class DescriptorDeclarationTest
+    {
+        static readonly FileDescriptor unitTestProto3Descriptor = LoadProtos();
+
+        [Test]
+        public void ServiceComments()
+        {
+            var service = unitTestProto3Descriptor.FindTypeByName<ServiceDescriptor>("TestService");
+            Assert.NotNull(service.Declaration);
+            Assert.AreEqual(" This is a test service\n", service.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void MethodComments()
+        {
+            var service = unitTestProto3Descriptor.FindTypeByName<ServiceDescriptor>("TestService");
+            var method = service.FindMethodByName("Foo");
+            Assert.NotNull(method.Declaration);
+            Assert.AreEqual(" This is a test method\n", method.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void MessageComments()
+        {
+            var message = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            Assert.NotNull(message.Declaration);
+            Assert.AreEqual(" This is a leading comment\n", message.Declaration.LeadingComments);
+            Assert.AreEqual(new[] { " This is leading detached comment 1\n", " This is leading detached comment 2\n" },
+                message.Declaration.LeadingDetachedComments);
+        }
+
+        [Test]
+        public void EnumComments()
+        {
+            var descriptor = unitTestProto3Descriptor.FindTypeByName<EnumDescriptor>("CommentEnum");
+            Assert.NotNull(descriptor.Declaration);
+            Assert.AreEqual(" Leading enum comment\n", descriptor.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void NestedMessageComments()
+        {
+            var outer = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            var nested = outer.FindDescriptor<MessageDescriptor>("NestedCommentMessage");
+            Assert.NotNull(nested.Declaration);
+            Assert.AreEqual(" Leading nested message comment\n", nested.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void NestedEnumComments()
+        {
+            var outer = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            var nested = outer.FindDescriptor<EnumDescriptor>("NestedCommentEnum");
+            Assert.NotNull(nested.Declaration);
+            Assert.AreEqual(" Leading nested enum comment\n", nested.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void FieldComments()
+        {
+            var message = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            var field = message.FindFieldByName("text");
+            Assert.NotNull(field.Declaration);
+            Assert.AreEqual(" Leading field comment\n", field.Declaration.LeadingComments);
+            Assert.AreEqual(" Trailing field comment\n", field.Declaration.TrailingComments);
+        }
+
+        [Test]
+        public void NestedMessageFieldComments()
+        {
+            var outer = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            var nested = outer.FindDescriptor<MessageDescriptor>("NestedCommentMessage");
+            var field = nested.FindFieldByName("nested_text");
+            Assert.NotNull(field.Declaration);
+            Assert.AreEqual(" Leading nested message field comment\n", field.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void EnumValueComments()
+        {
+            var enumDescriptor = unitTestProto3Descriptor.FindTypeByName<EnumDescriptor>("CommentEnum");
+            var value = enumDescriptor.FindValueByName("ZERO_VALUE");
+            Assert.NotNull(value.Declaration);
+            Assert.AreEqual(" Zero value comment\n", value.Declaration.LeadingComments);
+        }
+
+        [Test]
+        public void NestedEnumValueComments()
+        {
+            var outer = unitTestProto3Descriptor.FindTypeByName<MessageDescriptor>("CommentMessage");
+            var nested = outer.FindDescriptor<EnumDescriptor>("NestedCommentEnum");
+            var value = nested.FindValueByName("ZERO_VALUE");
+            Assert.NotNull(value.Declaration);
+            Assert.AreEqual(" Zero value comment\n", value.Declaration.LeadingComments);
+        }
+
+        private static FileDescriptor LoadProtos()
+        {
+            var type = typeof(DescriptorDeclarationTest);
+            // TODO: Make this simpler :)
+            FileDescriptorSet descriptorSet;
+            using (var stream = type.GetTypeInfo().Assembly.GetManifestResourceStream($"Google.Protobuf.Test.testprotos.pb"))
+            {
+                descriptorSet = FileDescriptorSet.Parser.ParseFrom(stream);
+            }
+            var byteStrings = descriptorSet.File.Select(f => f.ToByteString()).ToList();
+            var descriptors = FileDescriptor.BuildFromByteStrings(byteStrings);
+            return descriptors.Single(d => d.Name == "unittest_proto3.proto");
+        }
+    }
+}

+ 317 - 16
csharp/src/Google.Protobuf.Test/TestProtos/UnittestProto3.cs

@@ -139,23 +139,26 @@ namespace Google.Protobuf.TestProtos {
             "NBj//w8gAygEIigKG1Rlc3RDb21tZW50SW5qZWN0aW9uTWVzc2FnZRIJCgFh",
             "GAEgASgJIgwKCkZvb1JlcXVlc3QiDQoLRm9vUmVzcG9uc2UiEgoQRm9vQ2xp",
             "ZW50TWVzc2FnZSISChBGb29TZXJ2ZXJNZXNzYWdlIgwKCkJhclJlcXVlc3Qi",
-            "DQoLQmFyUmVzcG9uc2UiEgoQVGVzdEVtcHR5TWVzc2FnZSpZCgtGb3JlaWdu",
-            "RW51bRIXChNGT1JFSUdOX1VOU1BFQ0lGSUVEEAASDwoLRk9SRUlHTl9GT08Q",
-            "BBIPCgtGT1JFSUdOX0JBUhAFEg8KC0ZPUkVJR05fQkFaEAYqdQoUVGVzdEVu",
-            "dW1XaXRoRHVwVmFsdWUSKAokVEVTVF9FTlVNX1dJVEhfRFVQX1ZBTFVFX1VO",
-            "U1BFQ0lGSUVEEAASCAoERk9PMRABEggKBEJBUjEQAhIHCgNCQVoQAxIICgRG",
-            "T08yEAESCAoEQkFSMhACGgIQASqdAQoOVGVzdFNwYXJzZUVudW0SIAocVEVT",
-            "VF9TUEFSU0VfRU5VTV9VTlNQRUNJRklFRBAAEgwKCFNQQVJTRV9BEHsSDgoI",
-            "U1BBUlNFX0IQpucDEg8KCFNQQVJTRV9DELKxgAYSFQoIU1BBUlNFX0QQ8f//",
-            "////////ARIVCghTUEFSU0VfRRC03vz///////8BEgwKCFNQQVJTRV9HEAIy",
-            "nQEKC1Rlc3RTZXJ2aWNlEkYKA0ZvbxIeLnByb3RvYnVmX3VuaXR0ZXN0My5G",
-            "b29SZXF1ZXN0Gh8ucHJvdG9idWZfdW5pdHRlc3QzLkZvb1Jlc3BvbnNlEkYK",
-            "A0JhchIeLnByb3RvYnVmX3VuaXR0ZXN0My5CYXJSZXF1ZXN0Gh8ucHJvdG9i",
-            "dWZfdW5pdHRlc3QzLkJhclJlc3BvbnNlQixCDVVuaXR0ZXN0UHJvdG+qAhpH",
-            "b29nbGUuUHJvdG9idWYuVGVzdFByb3Rvc2IGcHJvdG8z"));
+            "DQoLQmFyUmVzcG9uc2UiEgoQVGVzdEVtcHR5TWVzc2FnZSJwCg5Db21tZW50",
+            "TWVzc2FnZRIMCgR0ZXh0GAEgASgJGisKFE5lc3RlZENvbW1lbnRNZXNzYWdl",
+            "EhMKC25lc3RlZF90ZXh0GAEgASgJIiMKEU5lc3RlZENvbW1lbnRFbnVtEg4K",
+            "ClpFUk9fVkFMVUUQACpZCgtGb3JlaWduRW51bRIXChNGT1JFSUdOX1VOU1BF",
+            "Q0lGSUVEEAASDwoLRk9SRUlHTl9GT08QBBIPCgtGT1JFSUdOX0JBUhAFEg8K",
+            "C0ZPUkVJR05fQkFaEAYqdQoUVGVzdEVudW1XaXRoRHVwVmFsdWUSKAokVEVT",
+            "VF9FTlVNX1dJVEhfRFVQX1ZBTFVFX1VOU1BFQ0lGSUVEEAASCAoERk9PMRAB",
+            "EggKBEJBUjEQAhIHCgNCQVoQAxIICgRGT08yEAESCAoEQkFSMhACGgIQASqd",
+            "AQoOVGVzdFNwYXJzZUVudW0SIAocVEVTVF9TUEFSU0VfRU5VTV9VTlNQRUNJ",
+            "RklFRBAAEgwKCFNQQVJTRV9BEHsSDgoIU1BBUlNFX0IQpucDEg8KCFNQQVJT",
+            "RV9DELKxgAYSFQoIU1BBUlNFX0QQ8f//////////ARIVCghTUEFSU0VfRRC0",
+            "3vz///////8BEgwKCFNQQVJTRV9HEAIqHQoLQ29tbWVudEVudW0SDgoKWkVS",
+            "T19WQUxVRRAAMp0BCgtUZXN0U2VydmljZRJGCgNGb28SHi5wcm90b2J1Zl91",
+            "bml0dGVzdDMuRm9vUmVxdWVzdBofLnByb3RvYnVmX3VuaXR0ZXN0My5Gb29S",
+            "ZXNwb25zZRJGCgNCYXISHi5wcm90b2J1Zl91bml0dGVzdDMuQmFyUmVxdWVz",
+            "dBofLnByb3RvYnVmX3VuaXR0ZXN0My5CYXJSZXNwb25zZUIsQg1Vbml0dGVz",
+            "dFByb3RvqgIaR29vZ2xlLlByb3RvYnVmLlRlc3RQcm90b3NiBnByb3RvMw=="));
       descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
           new pbr::FileDescriptor[] { global::Google.Protobuf.TestProtos.UnittestImportProto3Reflection.Descriptor, },
-          new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Google.Protobuf.TestProtos.ForeignEnum), typeof(global::Google.Protobuf.TestProtos.TestEnumWithDupValue), typeof(global::Google.Protobuf.TestProtos.TestSparseEnum), }, new pbr::GeneratedClrTypeInfo[] {
+          new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Google.Protobuf.TestProtos.ForeignEnum), typeof(global::Google.Protobuf.TestProtos.TestEnumWithDupValue), typeof(global::Google.Protobuf.TestProtos.TestSparseEnum), typeof(global::Google.Protobuf.TestProtos.CommentEnum), }, new pbr::GeneratedClrTypeInfo[] {
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.TestAllTypes), global::Google.Protobuf.TestProtos.TestAllTypes.Parser, new[]{ "SingleInt32", "SingleInt64", "SingleUint32", "SingleUint64", "SingleSint32", "SingleSint64", "SingleFixed32", "SingleFixed64", "SingleSfixed32", "SingleSfixed64", "SingleFloat", "SingleDouble", "SingleBool", "SingleString", "SingleBytes", "SingleNestedMessage", "SingleForeignMessage", "SingleImportMessage", "SingleNestedEnum", "SingleForeignEnum", "SingleImportEnum", "SinglePublicImportMessage", "RepeatedInt32", "RepeatedInt64", "RepeatedUint32", "RepeatedUint64", "RepeatedSint32", "RepeatedSint64", "RepeatedFixed32", "RepeatedFixed64", "RepeatedSfixed32", "RepeatedSfixed64", "RepeatedFloat", "RepeatedDouble", "RepeatedBool", "RepeatedString", "RepeatedBytes", "RepeatedNestedMessage", "RepeatedForeignMessage", "RepeatedImportMessage", "RepeatedNestedEnum", "RepeatedForeignEnum", "RepeatedImportEnum", "RepeatedPublicImportMessage", "OneofUint32", "OneofNestedMessage", "OneofString", "OneofBytes" }, new[]{ "OneofField" }, new[]{ typeof(global::Google.Protobuf.TestProtos.TestAllTypes.Types.NestedEnum) }, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.TestAllTypes.Types.NestedMessage), global::Google.Protobuf.TestProtos.TestAllTypes.Types.NestedMessage.Parser, new[]{ "Bb" }, null, null, null)}),
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.NestedTestAllTypes), global::Google.Protobuf.TestProtos.NestedTestAllTypes.Parser, new[]{ "Child", "Payload", "RepeatedChild" }, null, null, null),
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.TestDeprecatedFields), global::Google.Protobuf.TestProtos.TestDeprecatedFields.Parser, new[]{ "DeprecatedInt32" }, null, null, null),
@@ -190,7 +193,8 @@ namespace Google.Protobuf.TestProtos {
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.FooServerMessage), global::Google.Protobuf.TestProtos.FooServerMessage.Parser, null, null, null, null),
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.BarRequest), global::Google.Protobuf.TestProtos.BarRequest.Parser, null, null, null, null),
             new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.BarResponse), global::Google.Protobuf.TestProtos.BarResponse.Parser, null, null, null, null),
-            new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.TestEmptyMessage), global::Google.Protobuf.TestProtos.TestEmptyMessage.Parser, null, null, null, null)
+            new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.TestEmptyMessage), global::Google.Protobuf.TestProtos.TestEmptyMessage.Parser, null, null, null, null),
+            new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.CommentMessage), global::Google.Protobuf.TestProtos.CommentMessage.Parser, new[]{ "Text" }, null, new[]{ typeof(global::Google.Protobuf.TestProtos.CommentMessage.Types.NestedCommentEnum) }, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Google.Protobuf.TestProtos.CommentMessage.Types.NestedCommentMessage), global::Google.Protobuf.TestProtos.CommentMessage.Types.NestedCommentMessage.Parser, new[]{ "NestedText" }, null, null, null)})
           }));
     }
     #endregion
@@ -233,6 +237,16 @@ namespace Google.Protobuf.TestProtos {
     [pbr::OriginalName("SPARSE_G")] SparseG = 2,
   }
 
+  /// <summary>
+  /// Leading enum comment
+  /// </summary>
+  public enum CommentEnum {
+    /// <summary>
+    /// Zero value comment
+    /// </summary>
+    [pbr::OriginalName("ZERO_VALUE")] ZeroValue = 0,
+  }
+
   #endregion
 
   #region Messages
@@ -7301,6 +7315,293 @@ namespace Google.Protobuf.TestProtos {
 
   }
 
+  /// <summary>
+  /// This is a leading comment
+  /// </summary>
+  public sealed partial class CommentMessage : pb::IMessage<CommentMessage> {
+    private static readonly pb::MessageParser<CommentMessage> _parser = new pb::MessageParser<CommentMessage>(() => new CommentMessage());
+    private pb::UnknownFieldSet _unknownFields;
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public static pb::MessageParser<CommentMessage> Parser { get { return _parser; } }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Google.Protobuf.TestProtos.UnittestProto3Reflection.Descriptor.MessageTypes[35]; }
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public CommentMessage() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public CommentMessage(CommentMessage other) : this() {
+      text_ = other.text_;
+      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public CommentMessage Clone() {
+      return new CommentMessage(this);
+    }
+
+    /// <summary>Field number for the "text" field.</summary>
+    public const int TextFieldNumber = 1;
+    private string text_ = "";
+    /// <summary>
+    /// Leading field comment
+    /// </summary>
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public string Text {
+      get { return text_; }
+      set {
+        text_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
+      }
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public override bool Equals(object other) {
+      return Equals(other as CommentMessage);
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public bool Equals(CommentMessage other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (Text != other.Text) return false;
+      return Equals(_unknownFields, other._unknownFields);
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public override int GetHashCode() {
+      int hash = 1;
+      if (Text.Length != 0) hash ^= Text.GetHashCode();
+      if (_unknownFields != null) {
+        hash ^= _unknownFields.GetHashCode();
+      }
+      return hash;
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public override string ToString() {
+      return pb::JsonFormatter.ToDiagnosticString(this);
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (Text.Length != 0) {
+        output.WriteRawTag(10);
+        output.WriteString(Text);
+      }
+      if (_unknownFields != null) {
+        _unknownFields.WriteTo(output);
+      }
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public int CalculateSize() {
+      int size = 0;
+      if (Text.Length != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeStringSize(Text);
+      }
+      if (_unknownFields != null) {
+        size += _unknownFields.CalculateSize();
+      }
+      return size;
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public void MergeFrom(CommentMessage other) {
+      if (other == null) {
+        return;
+      }
+      if (other.Text.Length != 0) {
+        Text = other.Text;
+      }
+      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
+    }
+
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
+            break;
+          case 10: {
+            Text = input.ReadString();
+            break;
+          }
+        }
+      }
+    }
+
+    #region Nested types
+    /// <summary>Container for nested types declared in the CommentMessage message type.</summary>
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+    public static partial class Types {
+      /// <summary>
+      /// Leading nested enum comment
+      /// </summary>
+      public enum NestedCommentEnum {
+        /// <summary>
+        /// Zero value comment
+        /// </summary>
+        [pbr::OriginalName("ZERO_VALUE")] ZeroValue = 0,
+      }
+
+      /// <summary>
+      /// Leading nested message comment
+      /// </summary>
+      public sealed partial class NestedCommentMessage : pb::IMessage<NestedCommentMessage> {
+        private static readonly pb::MessageParser<NestedCommentMessage> _parser = new pb::MessageParser<NestedCommentMessage>(() => new NestedCommentMessage());
+        private pb::UnknownFieldSet _unknownFields;
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public static pb::MessageParser<NestedCommentMessage> Parser { get { return _parser; } }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public static pbr::MessageDescriptor Descriptor {
+          get { return global::Google.Protobuf.TestProtos.CommentMessage.Descriptor.NestedTypes[0]; }
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        pbr::MessageDescriptor pb::IMessage.Descriptor {
+          get { return Descriptor; }
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public NestedCommentMessage() {
+          OnConstruction();
+        }
+
+        partial void OnConstruction();
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public NestedCommentMessage(NestedCommentMessage other) : this() {
+          nestedText_ = other.nestedText_;
+          _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public NestedCommentMessage Clone() {
+          return new NestedCommentMessage(this);
+        }
+
+        /// <summary>Field number for the "nested_text" field.</summary>
+        public const int NestedTextFieldNumber = 1;
+        private string nestedText_ = "";
+        /// <summary>
+        /// Leading nested message field comment
+        /// </summary>
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public string NestedText {
+          get { return nestedText_; }
+          set {
+            nestedText_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
+          }
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public override bool Equals(object other) {
+          return Equals(other as NestedCommentMessage);
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public bool Equals(NestedCommentMessage other) {
+          if (ReferenceEquals(other, null)) {
+            return false;
+          }
+          if (ReferenceEquals(other, this)) {
+            return true;
+          }
+          if (NestedText != other.NestedText) return false;
+          return Equals(_unknownFields, other._unknownFields);
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public override int GetHashCode() {
+          int hash = 1;
+          if (NestedText.Length != 0) hash ^= NestedText.GetHashCode();
+          if (_unknownFields != null) {
+            hash ^= _unknownFields.GetHashCode();
+          }
+          return hash;
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public override string ToString() {
+          return pb::JsonFormatter.ToDiagnosticString(this);
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public void WriteTo(pb::CodedOutputStream output) {
+          if (NestedText.Length != 0) {
+            output.WriteRawTag(10);
+            output.WriteString(NestedText);
+          }
+          if (_unknownFields != null) {
+            _unknownFields.WriteTo(output);
+          }
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public int CalculateSize() {
+          int size = 0;
+          if (NestedText.Length != 0) {
+            size += 1 + pb::CodedOutputStream.ComputeStringSize(NestedText);
+          }
+          if (_unknownFields != null) {
+            size += _unknownFields.CalculateSize();
+          }
+          return size;
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public void MergeFrom(NestedCommentMessage other) {
+          if (other == null) {
+            return;
+          }
+          if (other.NestedText.Length != 0) {
+            NestedText = other.NestedText;
+          }
+          _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
+        }
+
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+        public void MergeFrom(pb::CodedInputStream input) {
+          uint tag;
+          while ((tag = input.ReadTag()) != 0) {
+            switch(tag) {
+              default:
+                _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
+                break;
+              case 10: {
+                NestedText = input.ReadString();
+                break;
+              }
+            }
+          }
+        }
+
+      }
+
+    }
+    #endregion
+
+  }
+
   #endregion
 
 }

BIN
csharp/src/Google.Protobuf.Test/testprotos.pb


+ 26 - 19
csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs

@@ -30,6 +30,8 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #endregion
 
+using System.Collections.Generic;
+
 namespace Google.Protobuf.Reflection
 {
     /// <summary>
@@ -37,15 +39,11 @@ namespace Google.Protobuf.Reflection
     /// </summary>
     public abstract class DescriptorBase : IDescriptor
     {
-        private readonly FileDescriptor file;
-        private readonly string fullName;
-        private readonly int index;
-
         internal DescriptorBase(FileDescriptor file, string fullName, int index)
         {
-            this.file = file;
-            this.fullName = fullName;
-            this.index = index;
+            File = file;
+            FullName = fullName;
+            Index = index;
         }
 
         /// <value>
@@ -56,10 +54,7 @@ namespace Google.Protobuf.Reflection
         /// this descriptor's type. (There can be duplicate values for different
         /// types, e.g. one enum type with index 0 and one message type with index 0.)
         /// </remarks>
-        public int Index
-        {
-            get { return index; }
-        }
+        public int Index { get; }
 
         /// <summary>
         /// Returns the name of the entity (field, message etc) being described.
@@ -69,17 +64,29 @@ namespace Google.Protobuf.Reflection
         /// <summary>
         /// The fully qualified name of the descriptor's target.
         /// </summary>
-        public string FullName
-        {
-            get { return fullName; }
-        }
+        public string FullName { get; }
 
         /// <value>
         /// The file this descriptor was declared in.
         /// </value>
-        public FileDescriptor File
-        {
-            get { return file; }
-        }
+        public FileDescriptor File { get; }
+
+        /// <summary>
+        /// The declaration information about the descriptor, or null if no declaration information
+        /// is available for this descriptor.
+        /// </summary>
+        /// <remarks>
+        /// This information is typically only available for dynamically loaded descriptors,
+        /// for example within a protoc plugin where the full descriptors, including source info,
+        /// are passed to the code by protoc.
+        /// </remarks>
+        public DescriptorDeclaration Declaration => File.GetDeclaration(this);
+
+        /// <summary>
+        /// Retrieves the list of nested descriptors corresponding to the given field number, if any.
+        /// If the field is unknown or not a nested descriptor list, return null to terminate the search.
+        /// The default implementation returns null.
+        /// </summary>
+        internal virtual IReadOnlyList<DescriptorBase> GetNestedDescriptorListForField(int fieldNumber) => null;
     }
 }

+ 106 - 0
csharp/src/Google.Protobuf/Reflection/DescriptorDeclaration.cs

@@ -0,0 +1,106 @@
+#region Copyright notice and license
+// Protocol Buffers - Google's data interchange format
+// Copyright 2018 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using static Google.Protobuf.Reflection.SourceCodeInfo.Types;
+
+namespace Google.Protobuf.Reflection
+{
+    /// <summary>
+    /// Provides additional information about the declaration of a descriptor,
+    /// such as source location and comments.
+    /// </summary>
+    public sealed class DescriptorDeclaration
+    {
+        /// <summary>
+        /// The descriptor this declaration relates to.
+        /// </summary>
+        public IDescriptor Descriptor { get; }
+
+        /// <summary>
+        /// The start line of the declaration within the source file. This value is 1-based.
+        /// </summary>
+        public int StartLine { get; }
+        /// <summary>
+        /// The start column of the declaration within the source file. This value is 1-based.
+        /// </summary>
+        public int StartColumn { get; }
+
+        /// <summary>
+        /// // The end line of the declaration within the source file. This value is 1-based.
+        /// </summary>
+        public int EndLine { get; }
+        /// <summary>
+        /// The end column of the declaration within the source file. This value is 1-based.
+        /// </summary>
+        public int EndColumn { get; }
+
+        /// <summary>
+        /// Comments appearing before the declaration. Never null, but may be empty.
+        /// </summary>
+        public string LeadingComments { get; }
+
+        /// <summary>
+        /// Comments appearing after the declaration. Never null, but may be empty.
+        /// </summary>
+        public string TrailingComments { get; }
+
+        /// <summary>
+        /// Comments appearing before the declaration, but separated from it by blank
+        /// lines. Each string represents a paragraph of comments. The list is never null,
+        /// but may be empty. Likewise each element is never null, but may be empty.
+        /// </summary>
+        public IReadOnlyList<string> LeadingDetachedComments { get; }
+
+        private DescriptorDeclaration(IDescriptor descriptor, Location location)
+        {
+            // TODO: Validation
+            Descriptor = descriptor;
+            bool hasEndLine = location.Span.Count == 4;
+            // Lines and columns are 0-based in the proto.
+            StartLine = location.Span[0] + 1;
+            StartColumn = location.Span[1] + 1;
+            EndLine = hasEndLine ? location.Span[2] + 1 : StartLine;
+            EndColumn = location.Span[hasEndLine ? 3 : 2] + 1;
+            LeadingComments = location.LeadingComments;
+            TrailingComments = location.TrailingComments;
+            LeadingDetachedComments = new ReadOnlyCollection<string>(location.LeadingDetachedComments.ToList());
+        }
+
+        internal static DescriptorDeclaration FromProto(IDescriptor descriptor, Location location) =>
+            new DescriptorDeclaration(descriptor, location);
+    }
+}

+ 11 - 0
csharp/src/Google.Protobuf/Reflection/EnumDescriptor.cs

@@ -72,6 +72,17 @@ namespace Google.Protobuf.Reflection
         /// </summary>
         public override string Name { get { return proto.Name; } }
 
+        internal override IReadOnlyList<DescriptorBase> GetNestedDescriptorListForField(int fieldNumber)
+        {
+            switch (fieldNumber)
+            {
+                case EnumDescriptorProto.ValueFieldNumber:
+                    return (IReadOnlyList<DescriptorBase>) Values;
+                default:
+                    return null;
+            }
+        }
+
         /// <summary>
         /// The CLR type for this enum. For generated code, this will be a CLR enum type.
         /// </summary>

+ 80 - 0
csharp/src/Google.Protobuf/Reflection/FileDescriptor.cs

@@ -34,7 +34,10 @@ using Google.Protobuf.WellKnownTypes;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
+using System.Threading;
+using static Google.Protobuf.Reflection.SourceCodeInfo.Types;
 
 namespace Google.Protobuf.Reflection
 {
@@ -55,6 +58,8 @@ namespace Google.Protobuf.Reflection
             ForceReflectionInitialization<Value.KindOneofCase>();
         }
 
+        private readonly Lazy<Dictionary<IDescriptor, DescriptorDeclaration>> declarations;
+
         private FileDescriptor(ByteString descriptorData, FileDescriptorProto proto, IEnumerable<FileDescriptor> dependencies, DescriptorPool pool, bool allowUnknownDependencies, GeneratedClrTypeInfo generatedCodeInfo)
         {
             SerializedData = descriptorData;
@@ -77,6 +82,81 @@ namespace Google.Protobuf.Reflection
             Services = DescriptorUtil.ConvertAndMakeReadOnly(proto.Service,
                                                              (service, index) =>
                                                              new ServiceDescriptor(service, this, index));
+
+            declarations = new Lazy<Dictionary<IDescriptor, DescriptorDeclaration>>(CreateDeclarationMap, LazyThreadSafetyMode.ExecutionAndPublication);
+        }
+
+        private Dictionary<IDescriptor, DescriptorDeclaration> CreateDeclarationMap()
+        {
+            var dictionary = new Dictionary<IDescriptor, DescriptorDeclaration>();
+            foreach (var location in Proto.SourceCodeInfo?.Location ?? Enumerable.Empty<Location>())
+            {
+                var descriptor = FindDescriptorForPath(location.Path);
+                if (descriptor != null)
+                {
+                    dictionary[descriptor] = DescriptorDeclaration.FromProto(descriptor, location);
+                }
+            }
+            return dictionary;
+
+            IDescriptor FindDescriptorForPath(IList<int> path)
+            {
+                // All complete declarations have an even, non-empty path length
+                // (There can be an empty path for a descriptor declaration, but that can't have any comments,
+                // so we currently ignore it.)
+                if (path.Count == 0 || (path.Count & 1) != 0)
+                {
+                    return null;
+                }
+                IReadOnlyList<DescriptorBase> topLevelList = GetNestedDescriptorListForField(path[0]);
+                DescriptorBase current = GetDescriptorFromList(topLevelList, path[1]);
+
+                for (int i = 2; current != null && i < path.Count; i += 2)
+                {
+                    var list = current.GetNestedDescriptorListForField(path[i]);
+                    current = GetDescriptorFromList(list, path[i + 1]);
+                }
+                return current;
+            }
+
+            DescriptorBase GetDescriptorFromList(IReadOnlyList<DescriptorBase> list, int index)
+            {
+                // This is fine: it may be a newer version of protobuf than we understand, with a new descriptor
+                // field.
+                if (list == null)
+                {
+                    return null;
+                }
+                // We *could* return null to silently continue, but this is basically data corruption.
+                if (index < 0 || index >= list.Count)
+                {
+                    // We don't have much extra information to give at this point unfortunately. If this becomes a problem,
+                    // we can pass in the complete path and report that and the file name.
+                    throw new InvalidProtocolBufferException($"Invalid descriptor location path: index out of range");
+                }
+                return list[index];
+            }
+
+            IReadOnlyList<DescriptorBase> GetNestedDescriptorListForField(int fieldNumber)
+            {
+                switch (fieldNumber)
+                {
+                    case FileDescriptorProto.ServiceFieldNumber:
+                        return (IReadOnlyList<DescriptorBase>) Services;
+                    case FileDescriptorProto.MessageTypeFieldNumber:
+                        return (IReadOnlyList<DescriptorBase>) MessageTypes;
+                    case FileDescriptorProto.EnumTypeFieldNumber:
+                        return (IReadOnlyList<DescriptorBase>) EnumTypes;
+                    default:
+                        return null;
+                }
+            }
+        }
+
+        internal DescriptorDeclaration GetDeclaration(IDescriptor descriptor)
+        {
+            declarations.Value.TryGetValue(descriptor, out var declaration);
+            return declaration;
         }
 
         /// <summary>

+ 15 - 0
csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs

@@ -115,6 +115,21 @@ namespace Google.Protobuf.Reflection
         /// </summary>
         public override string Name => Proto.Name;
 
+        internal override IReadOnlyList<DescriptorBase> GetNestedDescriptorListForField(int fieldNumber)
+        {
+            switch (fieldNumber)
+            {
+                case DescriptorProto.FieldFieldNumber:
+                    return (IReadOnlyList<DescriptorBase>) fieldsInDeclarationOrder;
+                case DescriptorProto.NestedTypeFieldNumber:
+                    return (IReadOnlyList<DescriptorBase>) NestedTypes;
+                case DescriptorProto.EnumTypeFieldNumber:
+                    return (IReadOnlyList<DescriptorBase>) EnumTypes;
+                default:
+                    return null;
+            }
+        }
+
         internal DescriptorProto Proto { get; }
 
         /// <summary>

+ 12 - 0
csharp/src/Google.Protobuf/Reflection/ServiceDescriptor.cs

@@ -32,6 +32,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 
 namespace Google.Protobuf.Reflection
 {
@@ -58,6 +59,17 @@ namespace Google.Protobuf.Reflection
         /// </summary>
         public override string Name { get { return proto.Name; } }
 
+        internal override IReadOnlyList<DescriptorBase> GetNestedDescriptorListForField(int fieldNumber)
+        {
+            switch (fieldNumber)
+            {
+                case ServiceDescriptorProto.MethodFieldNumber:
+                    return (IReadOnlyList<DescriptorBase>) methods;
+                default:
+                    return null;
+            }
+        }
+
         internal ServiceDescriptorProto Proto { get { return proto; } }
 
         /// <value>