Change:
<!-- Thank you for filing a bug report! Please provide a short summary of the bug, along with any information you feel relevant to replicating the bug. --I tried this code: /// let example = Example::new() /// .first("hello") #[cfgattr(not(feature = "one"), doc = " .second("hello")n")] /// .third("hello") /// .build(); /// with I expected to see this: ! Instead, this happened: ! <!-- If you're using the stable version of the compiler, you should also check if the bug also exists in the beta or nightly versions. --: running over the code gives me /// let example = Example::new() /// .first("hello") /// .second("hello") /// .third("hello") /// .build(); ///
As explained on discord, <strikethe attributes aren't applied to doc comments</strike(EDIT: they are applied, the problem seems to be that it results in two doc comments for rustdoc). However, your doc comment is split in two so the second half isn't considered as being inside a code block, explaining why you have this display. Even if shows it as one doc comment, rustdoc sees two of them. <strikeMaybe we should add a warning or something that attributes aren't applied to doc comments... What do you think ?</strike
How can an attribute get applied to a doc comment? Both are, under the hood, attributes, no? I would expect that to be equivalent to expecting to only derive Debug if is passed, which is not how that works. I'm guessing what is true is that attributes directly written don't actually merge with -style comments. Potentially relevant: , ,
Simplified reproducer rust //! foo(); #![doc = " bar();"] //! Checking with 1.31.0 it still ignores it (so likely not related), but checking with 1.21.0 (and 1.0.0) it worked as expected, so somewhere within that range (but I don't have any other versions installed at the moment to check).
Compilation broke somewhere between 1.23.0 (works) 1.24.0 (does not work) I tested it with rust //! let x = r#" #![doc = " bar();"] //! "#; //! assert_eq!(x, "n bar();n"); //!
They are not supposed to be applied to a doc comment I do not think that. Your example would make sense for which I also consider to be wrong, but I am talking about which should insert the attribute conditionally but that is not the point. It might help to illustrate my thought process; We have some kind of documented item: rust /// let example = Example::new() /// .first() /// .build(); /// and each of those is turned into a doc attribute rust"] #[doc = "let example = Example::new()"] #[doc = " .first()"] #[doc = " .build();"] #[doc = " In a later step those attributes would then be concatenated and parsed as markdown or whatever. I want to use this behavior to document items conditionally, depending on which features the crate user has enabled. One way to do this would be to do rust let example = Example::new() .first() .build(); rust let example = Example::new() .first() .second() .build(); but that would blow up the source code with many duplicate lines, where only some minor things were changed. So I thought that it might make more sense to conditionally include a doc comment like this: rust /// let example = Example::new() /// .first() #[cfgattr(feature = "one", doc = " .second()")] /// .build(); /// which would turn into rust"] #[doc = "let example = Example::new()"] #[doc = " .first()"] #[doc = " .second()"] #[doc = " .build();"] #[doc = " or rust"] #[doc = "let example = Example::new()"] #[doc = " .first()"] #[doc = " .build();"] #[doc = " depending on whether the feature is enabled. So the attribute should not be inserted and not applied to the following doc comment. The weird thing is that the manually expanded version is working as expected. rust"] #[doc = "let example = Example::new()"] #[doc = " .first()"] #[cfg_attr(feature = "one", doc = " .second()")] #[doc = " .build();"] #[doc = " but this is very unhandy to work with :/
I was randomly looking at passes for another reason and noticed what is likely the source of this issue. There are two relevant passes: runs second and removes leading columns of whitespace from the docs. This runs on a single "doc fragment" at a time, because of merging that normally means the entire documentation string, but since sugared and raw docs aren't merged they remain as separate fragments and use their own widths of leading whitespace to strip.
That also matches up with first released in 1.24 which introduced this distinction between doc kinds in order to fix , which IMO was user error in not matching the indent depth.

Discussion:
use std::cmp; <del> use std::string::String; use crate::clean::{self, DocFragment, Item}; </del> <ins> use crate::clean::{self, DocFragment, DocFragmentKind, Item}; </ins> use crate::core::DocContext; use crate::fold::{self, DocFolder}; use crate::passes::Pass;

} fn unindent_fragments(docs: &mut Vec<DocFragment>) { <del> for fragment in docs { fragment.doc = unindent(&fragment.doc); } } fn unindent(s: &str) -> String { let lines = s.lines().collect::<Vec<&str>>(); let mut saw_first_line = false; let mut saw_second_line = false; let min_indent = lines.iter().fold(usize::MAX, |min_indent, line| { // After we see the first non-whitespace line, look at // the line we have. If it is not whitespace, and therefore // part of the first paragraph, then ignore the indentation // level of the first line let ignore_previous_indents = saw_first_line && !saw_second_line && !line.chars().all(|c| c.is_whitespace()); </del> <ins> // `add` is used in case the most common sugared doc syntax is used ("/// "). The other // fragments kind's lines are never starting with a whitespace unless they are using some // markdown formatting requiring it. Therefore, if the doc block have a mix between the two, // we need to take into account the fact that the minimum indent minus one (to take this // whitespace into account). // // For example: // // /// hello! // #[doc = "another"] // // In this case, you want "hello! another" and not "hello!  another". let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind) && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc) { // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to // "decide" how much the minimum indent will be. 1 } else { 0 }; </ins> <del> let min_indent = if ignore_previous_indents { usize::MAX } else { min_indent }; </del> <ins> // `min_indent` is used to know how much whitespaces from the start of each lines must be // removed. Example: // // ///     hello! // #[doc = "another"] // // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4 // (5 - 1) whitespaces. let min_indent = match docs .iter() .map(|fragment| { fragment.doc.lines().fold(usize::MAX, |min_indent, line| { if line.chars().all(|c| c.is_whitespace()) { min_indent } else { // Compare against either space or tab, ignoring whether they are // mixed or not. let whitespace = line.chars().take_while(|c| *c == ' ' || *c == 't').count(); cmp::min(min_indent, whitespace) + if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add } } }) }) .min() { Some(x) => x, None => return, }; </ins> <del> if saw_first_line { saw_second_line = true; </del> <ins> for fragment in docs { if fragment.doc.lines().count() == 0 { continue; </ins> } <del> if line.chars().all(|c| c.is_whitespace()) { min_indent </del> <ins> let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 { min_indent - add </ins> } else { <del> saw_first_line = true; let mut whitespace = 0; line.chars().all(|char| { // Compare against either space or tab, ignoring whether they // are mixed or not if char == ' ' || char == 't' { whitespace += 1; true </del> <ins> min_indent }; fragment.doc = fragment .doc .lines() .map(|line| { if line.chars().all(|c| c.is_whitespace()) { line.to_string() </ins> } else { <del> false </del> <ins> assert!(line.len() >= min_indent); line[min_indent..].to_string() </ins> } <del> }); cmp::min(min_indent, whitespace) } }); if !lines.is_empty() { let mut unindented = vec![lines[0].trim_start().to_string()]; unindented.extend_from_slice( &lines[1..] .iter() .map(|&line| { if line.chars().all(|c| c.is_whitespace()) { line.to_string() } else { assert!(line.len() >= min_indent); line[min_indent..].to_string() } }) .collect::<Vec<_>>(), ); unindented.join("n") } else { s.to_string() </del> <ins> }) .collect::<Vec<_>>() .join("n"); </ins> } }

use super::*; <ins> use rustc_span::source_map::DUMMY_SP; fn create_doc_fragment(s: &str) -> Vec<DocFragment> { vec![DocFragment { line: 0, span: DUMMY_SP, parent_module: None, doc: s.to_string(), kind: DocFragmentKind::SugaredDoc, }] } #[track_caller] fn run_test(input: &str, expected: &str) { let mut s = create_doc_fragment(input); unindent_fragments(&mut s); assert_eq!(s[0].doc, expected); } </ins> #[test] fn should_unindent() { <del> let s = "    line1n    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nline2"); </del> <ins> run_test("    line1n    line2", "line1nline2"); </ins> } #[test] fn should_unindent_multiple_paragraphs() { <del> let s = "    line1nn    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nnline2"); </del> <ins> run_test("    line1nn    line2", "line1nnline2"); </ins> } #[test] fn should_leave_multiple_indent_levels() { // Line 2 is indented another level beyond the // base indentation and should be preserved <del> let s = "    line1nn        line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nn    line2"); </del> <ins> run_test("    line1nn        line2", "line1nn    line2"); </ins> } #[test] fn should_ignore_first_line_indent() { <del> // The first line of the first paragraph may not be indented as // far due to the way the doc string was written: // // #[doc = "Start way over here //          and continue here"] let s = "line1n    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nline2"); </del> <ins> run_test("line1n    line2", "line1n    line2"); </ins> } #[test] fn should_not_ignore_first_line_indent_in_a_single_line_para() { <del> let s = "line1nn    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nn    line2"); </del> <ins> run_test("line1nn    line2", "line1nn    line2"); </ins> } #[test] fn should_unindent_tabs() { <del> let s = "tline1ntline2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nline2"); </del> <ins> run_test("tline1ntline2", "line1nline2"); </ins> } #[test] fn should_trim_mixed_indentation() { <del> let s = "t    line1nt    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nline2"); let s = "    tline1n    tline2".to_string(); let r = unindent(&s); assert_eq!(r, "line1nline2"); </del> <ins> run_test("t    line1nt    line2", "line1nline2"); run_test("    tline1n    tline2", "line1nline2"); </ins> } #[test] fn should_not_trim() { <del> let s = "t    line1  nt    line2".to_string(); let r = unindent(&s); assert_eq!(r, "line1  nline2"); let s = "    tline1  n    tline2".to_string(); let r = unindent(&s); assert_eq!(r, "line1  nline2"); </del> <ins> run_test("t    line1  nt    line2", "line1  nline2"); run_test("    tline1  n    tline2", "line1  nline2"); </ins> }

<ins> #![feature(external_doc)] #![crate_name = "foo"] // @has foo/struct.Example.html // @matches - '//pre[@class="rust rust-example-rendered"]'  //     '(?m)let example = Example::new()n    .first()n    .second()n    .build();Z' /// ```rust /// let example = Example::new() ///     .first() #[cfg_attr(not(feature = "one"), doc = "    .second()")] ///     .build(); /// ``` pub struct Example; // @has foo/struct.F.html // @matches - '//pre[@class="rust rust-example-rendered"]'  //     '(?m)let example = Example::new()n    .first()n    .another()n    .build();Z' ///```rust ///let example = Example::new() ///    .first() #[cfg_attr(not(feature = "one"), doc = "    .another()")] ///    .build(); /// ``` pub struct F; // @has foo/struct.G.html // @matches - '//pre[@class="rust rust-example-rendered"]'  //     '(?m)let example = Example::new()n.first()n    .another()n.build();Z' ///```rust ///let example = Example::new() ///.first() #[cfg_attr(not(feature = "one"), doc = "    .another()")] ///.build(); ///``` pub struct G; // @has foo/struct.H.html // @has - '//div[@class="docblock"]/p' 'no whitespace lol' ///no whitespace #[doc = " lol"] pub struct H; // @has foo/struct.I.html // @matches - '//pre[@class="rust rust-example-rendered"]' '(?m)4 whitespaces!Z' ///     4 whitespaces! #[doc = "something"] pub struct I; // @has foo/struct.J.html // @matches - '//div[@class="docblock"]/p' '(?m)anno whitespacenJust some text.Z' ///a ///no whitespace #[doc(include = "unindent.md")] pub struct J; // @has foo/struct.K.html // @matches - '//pre[@class="rust rust-example-rendered"]' '(?m)4 whitespaces!Z' ///a /// ///    4 whitespaces! /// #[doc(include = "unindent.md")] pub struct K; </ins>