diff --git a/Configurations.md b/Configurations.md index 6fcd0609b656..8861d7b2a136 100644 --- a/Configurations.md +++ b/Configurations.md @@ -2188,6 +2188,32 @@ If you want to format code that requires edition 2018, add the following to your edition = "2018" ``` +## `normalize_doc_attributes` + +Convert `#![doc]` and `#[doc]` attributes to `//!` and `///` doc comments. + +- **Default value**: `false` +- **Possible values**: `true`, `false` +- **Stable**: No + +#### `false` (default): + +```rust +#![doc = "Example documentation"] + +#[doc = "Example item documentation"] +pub enum Foo {} +``` + +#### `true`: + +```rust +//! Example documentation + +/// Example item documentation +pub enum Foo {} +``` + ## `emit_mode` Internal option diff --git a/src/attr.rs b/src/attr.rs index fc29c69b498c..f8f6cd1c3760 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -10,7 +10,7 @@ //! Format attributes and meta items. -use comment::{contains_comment, rewrite_doc_comment}; +use comment::{contains_comment, rewrite_doc_comment, CommentStyle}; use config::lists::*; use config::IndentStyle; use expr::rewrite_literal; @@ -350,13 +350,34 @@ impl Rewrite for ast::Attribute { if contains_comment(snippet) { return Some(snippet.to_owned()); } - // 1 = `[` - let shape = shape.offset_left(prefix.len() + 1)?; - Some( - self.meta() - .and_then(|meta| meta.rewrite(context, shape)) - .map_or_else(|| snippet.to_owned(), |rw| format!("{}[{}]", prefix, rw)), - ) + + if let Some(ref meta) = self.meta() { + // This attribute is possibly a doc attribute needing normalization to a doc comment + if context.config.normalize_doc_attributes() && meta.check_name("doc") { + if let Some(ref literal) = meta.value_str() { + let comment_style = match self.style { + ast::AttrStyle::Inner => CommentStyle::Doc, + ast::AttrStyle::Outer => CommentStyle::TripleSlash, + }; + + let doc_comment = format!("{}{}", comment_style.opener(), literal); + return rewrite_doc_comment( + &doc_comment, + shape.comment(context.config), + context.config, + ); + } + } + + // 1 = `[` + let shape = shape.offset_left(prefix.len() + 1)?; + Some( + meta.rewrite(context, shape) + .map_or_else(|| snippet.to_owned(), |rw| format!("{}[{}]", prefix, rw)), + ) + } else { + Some(snippet.to_owned()) + } } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index f240c7b13c68..52401a18d92e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -49,6 +49,7 @@ create_config! { comment_width: usize, 80, false, "Maximum length of comments. No effect unless wrap_comments = true"; normalize_comments: bool, false, false, "Convert /* */ comments to // comments where possible"; + normalize_doc_attributes: bool, false, false, "Normalize doc attributes as doc comments"; license_template_path: String, String::default(), false, "Beginning of file must match license template"; format_strings: bool, false, false, "Format string literals where necessary"; diff --git a/tests/source/attrib.rs b/tests/source/attrib.rs index 113464112695..d607b7e6f2f3 100644 --- a/tests/source/attrib.rs +++ b/tests/source/attrib.rs @@ -30,6 +30,7 @@ impl Bar { /// Blah blah blooo. /// Blah blah blooo. #[an_attribute] + #[doc = "an attribute that shouldn't be normalized to a doc comment"] fn foo(&mut self) -> isize { } diff --git a/tests/source/doc-attrib.rs b/tests/source/doc-attrib.rs new file mode 100644 index 000000000000..363e1441da3e --- /dev/null +++ b/tests/source/doc-attrib.rs @@ -0,0 +1,91 @@ +// rustfmt-wrap_comments: true +// rustfmt-normalize_doc_attributes: true + +// Only doc = "" attributes should be normalized +#![doc = "Example doc attribute comment"] +#![doc(html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png", + html_favicon_url = "https://doc.rust-lang.org/favicon.ico", + html_root_url = "https://doc.rust-lang.org/nightly/", + html_playground_url = "https://play.rust-lang.org/", test(attr(deny(warnings))))] + + +// Long `#[doc = "..."]` +struct A { #[doc = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] b: i32 } + + +#[doc = "The `nodes` and `edges` method each return instantiations of `Cow<[T]>` to leave implementers the freedom to create entirely new vectors or to pass back slices into internally owned vectors."] +struct B { b: i32 } + + +#[doc = "Level 1 comment"] +mod tests { + #[doc = "Level 2 comment"] + impl A { + #[doc = "Level 3 comment"] + fn f() { + #[doc = "Level 4 comment"] + fn g() { + } + } + } +} + +struct C { + #[doc = "item doc attrib comment"] + // regular item comment + b: i32, + + // regular item comment + #[doc = "item doc attrib comment"] + c: i32, +} + +// non-regression test for regular attributes, from #2647 +#[cfg(feature = "this_line_is_101_characters_long_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")] +pub fn foo() {} + +// path attrs +#[clippy::bar] +#[clippy::bar=foo] +#[clippy::bar(a, b, c)] +pub fn foo() {} + +mod issue_2620 { + #[derive(Debug, StructOpt)] +#[structopt(about = "Display information about the character on FF Logs")] +pub struct Params { + #[structopt(help = "The server the character is on")] + server: String, + #[structopt(help = "The character's first name")] + first_name: String, + #[structopt(help = "The character's last name")] + last_name: String, + #[structopt( + short = "j", + long = "job", + help = "The job to look at", + parse(try_from_str) + )] + job: Option +} +} + +// non-regression test for regular attributes, from #2969 +#[cfg(not(all(feature="std", + any(target_os = "linux", target_os = "android", + target_os = "netbsd", + target_os = "dragonfly", + target_os = "haiku", + target_os = "emscripten", + target_os = "solaris", + target_os = "cloudabi", + target_os = "macos", target_os = "ios", + target_os = "freebsd", + target_os = "openbsd", target_os = "bitrig", + target_os = "redox", + target_os = "fuchsia", + windows, + all(target_arch = "wasm32", feature = "stdweb"), + all(target_arch = "wasm32", feature = "wasm-bindgen"), + ))))] +type Os = NoSource; diff --git a/tests/target/attrib.rs b/tests/target/attrib.rs index 84841f33d8ef..942f669d4956 100644 --- a/tests/target/attrib.rs +++ b/tests/target/attrib.rs @@ -33,6 +33,7 @@ impl Bar { /// Blah blah blooo. /// Blah blah blooo. #[an_attribute] + #[doc = "an attribute that shouldn't be normalized to a doc comment"] fn foo(&mut self) -> isize {} /// Blah blah bing. diff --git a/tests/target/doc-attrib.rs b/tests/target/doc-attrib.rs new file mode 100644 index 000000000000..044e004decc2 --- /dev/null +++ b/tests/target/doc-attrib.rs @@ -0,0 +1,105 @@ +// rustfmt-wrap_comments: true +// rustfmt-normalize_doc_attributes: true + +// Only doc = "" attributes should be normalized +//! Example doc attribute comment +#![doc( + html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png", + html_favicon_url = "https://doc.rust-lang.org/favicon.ico", + html_root_url = "https://doc.rust-lang.org/nightly/", + html_playground_url = "https://play.rust-lang.org/", + test(attr(deny(warnings))) +)] + +// Long `#[doc = "..."]` +struct A { + /// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + b: i32, +} + +/// The `nodes` and `edges` method each return instantiations of `Cow<[T]>` to +/// leave implementers the freedom to create entirely new vectors or to pass +/// back slices into internally owned vectors. +struct B { + b: i32, +} + +/// Level 1 comment +mod tests { + /// Level 2 comment + impl A { + /// Level 3 comment + fn f() { + /// Level 4 comment + fn g() {} + } + } +} + +struct C { + /// item doc attrib comment + // regular item comment + b: i32, + + // regular item comment + /// item doc attrib comment + c: i32, +} + +// non-regression test for regular attributes, from #2647 +#[cfg( + feature = "this_line_is_101_characters_long_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +)] +pub fn foo() {} + +// path attrs +#[clippy::bar] +#[clippy::bar=foo] +#[clippy::bar(a, b, c)] +pub fn foo() {} + +mod issue_2620 { + #[derive(Debug, StructOpt)] + #[structopt(about = "Display information about the character on FF Logs")] + pub struct Params { + #[structopt(help = "The server the character is on")] + server: String, + #[structopt(help = "The character's first name")] + first_name: String, + #[structopt(help = "The character's last name")] + last_name: String, + #[structopt( + short = "j", + long = "job", + help = "The job to look at", + parse(try_from_str) + )] + job: Option, + } +} + +// non-regression test for regular attributes, from #2969 +#[cfg(not(all( + feature = "std", + any( + target_os = "linux", + target_os = "android", + target_os = "netbsd", + target_os = "dragonfly", + target_os = "haiku", + target_os = "emscripten", + target_os = "solaris", + target_os = "cloudabi", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "openbsd", + target_os = "bitrig", + target_os = "redox", + target_os = "fuchsia", + windows, + all(target_arch = "wasm32", feature = "stdweb"), + all(target_arch = "wasm32", feature = "wasm-bindgen"), + ) +)))] +type Os = NoSource;