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 + /// - 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 \n".to_string(), + }, + TestCases { + name: "valid_https", + input: "See https://www.rust-lang.org/".to_string(), + expected: "///See \n".to_string(), + }, + TestCases { + name: "valid_https_parenthesis", + input: "See (https://www.rust-lang.org/)".to_string(), + expected: "///See ()\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 +// // 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 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]() 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, - /// Included interfaces. See [Mixin][]. + /// Included interfaces. See \[Mixin][\]. #[prost(message, repeated, tag="6")] pub mixins: ::prost::alloc::vec::Vec, /// 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](). /// /// 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]() 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]() 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()\]() /// 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`\]() 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()`\]( +/// /// ) to obtain a formatter capable of generating timestamps in this format. /// ///