Repository: tokio-rs/prost (language: rust)

Issue:
Escape brackets in the doc
I have a source file containing `[EXPERIMENTAL]` markers in the comments. This gets translated to doc comment without escaping leading to false links and warnings. Escaping it (`\[`, `\]`) would solve the issue.


Discussion / hints:

Fix patch:
diff --git a/prost-build/Cargo.toml b/prost-build/Cargo.toml
index 223b96b4e..24faedc21 100644
--- a/prost-build/Cargo.toml
+++ b/prost-build/Cargo.toml
@@ -22,6 +22,8 @@ petgraph = { version = "0.6", default-features = false }
 prost = { version = "0.8.0", path = "..", default-features = false }
 prost-types = { version = "0.8.0", path = "../prost-types", default-features = false }
 tempfile = "3"
+lazy_static = "1.4.0"
+regex = "1.5.4"
 
 [build-dependencies]
 which = { version = "4", default-features = false }
diff --git a/prost-build/src/ast.rs b/prost-build/src/ast.rs
index 0e3dbdc3e..e75a0f5f7 100644
--- a/prost-build/src/ast.rs
+++ b/prost-build/src/ast.rs
@@ -1,4 +1,6 @@
+use lazy_static::lazy_static;
 use prost_types::source_code_info::Location;
+use regex::Regex;
 
 /// Comments on a Protobuf item.
 #[derive(Debug)]
@@ -53,7 +55,7 @@ impl Comments {
                     buf.push_str("    ");
                 }
                 buf.push_str("//");
-                buf.push_str(line);
+                buf.push_str(&Self::sanitize_line(line));
                 buf.push('\n');
             }
             buf.push('\n');
@@ -65,7 +67,7 @@ impl Comments {
                 buf.push_str("    ");
             }
             buf.push_str("///");
-            buf.push_str(line);
+            buf.push_str(&Self::sanitize_line(line));
             buf.push('\n');
         }
 
@@ -83,10 +85,24 @@ impl Comments {
                 buf.push_str("    ");
             }
             buf.push_str("///");
-            buf.push_str(line);
+            buf.push_str(&Self::sanitize_line(line));
             buf.push('\n');
         }
     }
+
+    /// Sanitizes the line for rustdoc by performing the following operations:
+    ///     - escape urls as <http://foo.com>
+    ///     - escape `[` & `]`
+    fn sanitize_line(line: &str) -> String {
+        lazy_static! {
+            static ref RULE_URL: Regex = Regex::new(r"https?://[^\s)]+").unwrap();
+            static ref RULE_BRACKETS: Regex = Regex::new(r"(\[)(\S+)(])").unwrap();
+        }
+
+        let mut s = RULE_URL.replace_all(line, r"<$0>").to_string();
+        s = RULE_BRACKETS.replace_all(&s, r"\$1$2\$3").to_string();
+        s
+    }
 }
 
 /// A service descriptor.
@@ -130,3 +146,96 @@ pub struct Method {
     /// Identifies if server streams multiple server messages.
     pub server_streaming: bool,
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_comment_append_with_indent_sanitizes_comment_doc_url() {
+        struct TestCases {
+            name: &'static str,
+            input: String,
+            expected: String,
+        }
+
+        let tests = vec![
+            TestCases {
+                name: "valid_http",
+                input: "See https://www.rust-lang.org/".to_string(),
+                expected: "///See <https://www.rust-lang.org/>\n".to_string(),
+            },
+            TestCases {
+                name: "valid_https",
+                input: "See https://www.rust-lang.org/".to_string(),
+                expected: "///See <https://www.rust-lang.org/>\n".to_string(),
+            },
+            TestCases {
+                name: "valid_https_parenthesis",
+                input: "See (https://www.rust-lang.org/)".to_string(),
+                expected: "///See (<https://www.rust-lang.org/>)\n".to_string(),
+            },
+            TestCases {
+                name: "invalid",
+                input: "See note://abc".to_string(),
+                expected: "///See note://abc\n".to_string(),
+            },
+        ];
+        for t in tests {
+            let input = Comments {
+                leading_detached: vec![],
+                leading: vec![],
+                trailing: vec![t.input],
+            };
+
+            let mut actual = "".to_string();
+            input.append_with_indent(0, &mut actual);
+
+            assert_eq!(t.expected, actual, "failed {}", t.name);
+        }
+    }
+
+    #[test]
+    fn test_comment_append_with_indent_sanitizes_square_brackets() {
+        struct TestCases {
+            name: &'static str,
+            input: String,
+            expected: String,
+        }
+
+        let tests = vec![
+            TestCases {
+                name: "valid_brackets",
+                input: "foo [bar] baz".to_string(),
+                expected: "///foo \\[bar\\] baz\n".to_string(),
+            },
+            TestCases {
+                name: "invalid_start_bracket",
+                input: "foo [= baz".to_string(),
+                expected: "///foo [= baz\n".to_string(),
+            },
+            TestCases {
+                name: "invalid_end_bracket",
+                input: "foo =] baz".to_string(),
+                expected: "///foo =] baz\n".to_string(),
+            },
+            TestCases {
+                name: "invalid_bracket_combination",
+                input: "[0, 9)".to_string(),
+                expected: "///[0, 9)\n".to_string(),
+            },
+        ];
+        for t in tests {
+            let input = Comments {
+                leading_detached: vec![],
+                leading: vec![],
+                trailing: vec![t.input],
+            };
+
+            let mut actual = "".to_string();
+            input.append_with_indent(0, &mut actual);
+
+            assert_eq!(t.expected, actual, "failed {}", t.name);
+        }
+    }
+}
diff --git a/prost-types/src/protobuf.rs b/prost-types/src/protobuf.rs
index 7530b7827..1440b4c00 100644
--- a/prost-types/src/protobuf.rs
+++ b/prost-types/src/protobuf.rs
@@ -331,7 +331,7 @@ pub struct MethodDescriptorProto {
 //   extension number. You can declare multiple options with only one extension
 //   number by putting them in a sub-message. See the Custom Options section of
 //   the docs for examples:
-//   https://developers.google.com/protocol-buffers/docs/proto#options
+//   <https://developers.google.com/protocol-buffers/docs/proto#options>
 //   If this turns out to be popular, a web service will be set up
 //   to automatically assign option numbers.
 
@@ -989,7 +989,7 @@ pub mod generated_code_info {
 /// If the embedded message type is well-known and has a custom JSON
 /// representation, that representation will be embedded adding a field
 /// `value` which holds the custom JSON in addition to the `@type`
-/// field. Example (for message [google.protobuf.Duration][]):
+/// field. Example (for message \[google.protobuf.Duration][\]):
 ///
 ///     {
 ///       "@type": "type.googleapis.com/google.protobuf.Duration",
@@ -1011,7 +1011,7 @@ pub struct Any {
     /// server that maps type URLs to message definitions as follows:
     ///
     /// * If no scheme is provided, `https` is assumed.
-    /// * An HTTP GET on the URL must yield a [google.protobuf.Type][]
+    /// * An HTTP GET on the URL must yield a \[google.protobuf.Type][\]
     ///   value in binary format, or produce an error.
     /// * Applications are allowed to cache lookup results based on the
     ///   URL, or have them precompiled into a binary to avoid any
@@ -1223,7 +1223,7 @@ pub enum Syntax {
 /// from API Services, which represent a concrete implementation of an interface
 /// as opposed to simply a description of methods and bindings. They are also
 /// sometimes simply referred to as "APIs" in other contexts, such as the name of
-/// this message itself. See https://cloud.google.com/apis/design/glossary for
+/// this message itself. See <https://cloud.google.com/apis/design/glossary> for
 /// detailed terminology.
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct Api {
@@ -1245,7 +1245,7 @@ pub struct Api {
     /// consistent with what is provided here.
     ///
     /// The versioning schema uses [semantic
-    /// versioning](http://semver.org) where the major version number
+    /// versioning](<http://semver.org>) where the major version number
     /// indicates a breaking change and the minor version an additive,
     /// non-breaking change. Both version numbers are signals to users
     /// what to expect from different versions, and should be carefully
@@ -1264,7 +1264,7 @@ pub struct Api {
     /// message.
     #[prost(message, optional, tag="5")]
     pub source_context: ::core::option::Option<SourceContext>,
-    /// Included interfaces. See [Mixin][].
+    /// Included interfaces. See \[Mixin][\].
     #[prost(message, repeated, tag="6")]
     pub mixins: ::prost::alloc::vec::Vec<Mixin>,
     /// The source syntax of the service.
@@ -1310,7 +1310,7 @@ pub struct Method {
 ///
 /// - If an http annotation is inherited, the path pattern will be
 ///   modified as follows. Any version prefix will be replaced by the
-///   version of the including interface plus the [root][] path if
+///   version of the including interface plus the \[root][\] path if
 ///   specified.
 ///
 /// Example of a simple mixin:
@@ -1543,7 +1543,7 @@ pub struct Duration {
 ///         d: 1
 ///         x: 2
 ///       }
-///       c: [1]
+///       c: \[1\]
 ///     }
 ///
 /// And an update message:
@@ -1552,7 +1552,7 @@ pub struct Duration {
 ///       b {
 ///         d: 10
 ///       }
-///       c: [2]
+///       c: \[2\]
 ///     }
 ///
 /// then if the field mask is:
@@ -1743,11 +1743,11 @@ pub enum NullValue {
 ///
 /// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
 /// second table is needed for interpretation, using a [24-hour linear
-/// smear](https://developers.google.com/time/smear).
+/// smear](<https://developers.google.com/time/smear>).
 ///
 /// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
 /// restricting to that range, we ensure that we can convert to and from [RFC
-/// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
+/// 3339](<https://www.ietf.org/rfc/rfc3339.txt>) date strings.
 ///
 /// # Examples
 ///
@@ -1803,8 +1803,8 @@ pub enum NullValue {
 /// # JSON Mapping
 ///
 /// In JSON format, the Timestamp type is encoded as a string in the
-/// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
-/// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
+/// [RFC 3339](<https://www.ietf.org/rfc/rfc3339.txt>) format. That is, the
+/// format is "{year}-{month}-{day}T{hour}:{min}:{sec}\[.{frac_sec}\]Z"
 /// where {year} is always expressed using four digits while {month}, {day},
 /// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
 /// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
@@ -1818,13 +1818,13 @@ pub enum NullValue {
 ///
 /// In JavaScript, one can convert a Date object to this format using the
 /// standard
-/// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
+/// \[toISOString()\](<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString>)
 /// method. In Python, a standard `datetime.datetime` object can be converted
 /// to this format using
-/// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
+/// \[`strftime`\](<https://docs.python.org/2/library/time.html#time.strftime>) with
 /// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
-/// the Joda Time's [`ISODateTimeFormat.dateTime()`](
-/// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
+/// the Joda Time's \[`ISODateTimeFormat.dateTime()`\](
+/// <http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D>
 /// ) to obtain a formatter capable of generating timestamps in this format.
 ///
 ///


Test patch: