Repository: rust-lang/rustfmt (rust)

Issue:
`wrap_comments` wraps (long) headers
Input (from the `miette` crate's docs):
```rust
/// # Wrapping Types That Do Not Implement `Error` (e.g. `&str` and `Box<dyn Error>`)
```

After `cargo fmt` with `rustfmt.toml`:
```toml
edition = "2021"
wrap_comments = true
format_code_in_doc_comments = true
```

Output:
```rust
/// # Wrapping Types That Do Not Implement `Error` (e.g. `&str` and `Box<dyn
/// Error>`)
```

Which renders as:
<img width="780" alt="Screen Shot 2022-02-25 at 15 57 09" src="https://user-images.githubusercontent.com/387036/155737116-c451468e-bdde-4618-a518-1f03542216a2.png">

# Fix

Do not ever break comment lines starting with `#` even when `wrap_comments` is set.

<!-- TRIAGEBOT_START -->

<!-- TRIAGEBOT_ASSIGN_START -->

<!-- TRIAGEBOT_ASSIGN_DATA_START$${"user":"ytmimi"}$$TRIAGEBOT_ASSIGN_DATA_END -->

<!-- TRIAGEBOT_ASSIGN_END -->
<!-- TRIAGEBOT_END -->

Pull request:
Prevent wrapping markdown headers in doc comments
Fixes #5238

A markdown header is defined by a string that starts with `#`.

Previously, rustfmt would wrap long markdown headers when
`wrap_comments=true`. This lead to issues when rendering these headers
in HTML using rustdoc.

Now, rustfmt leaves markdown headers alone when wrapping comments.

Patch:
diff --git a/src/comment.rs b/src/comment.rs
index 96778c4e..f9d8a0fa 100644
--- a/src/comment.rs
+++ b/src/comment.rs
@@ -683,6 +683,7 @@ impl<'a> CommentRewrite<'a> {
         i: usize,
         line: &'a str,
         has_leading_whitespace: bool,
+        is_doc_comment: bool,
     ) -> bool {
         let num_newlines = count_newlines(orig);
         let is_last = i == num_newlines;
@@ -789,10 +790,19 @@ impl<'a> CommentRewrite<'a> {
             }
         }
 
-        if self.fmt.config.wrap_comments()
+        let is_markdown_header_doc_comment = is_doc_comment && line.starts_with("#");
+
+        // We only want to wrap the comment if:
+        // 1) wrap_comments = true is configured
+        // 2) The comment is not the start of a markdown header doc comment
+        // 3) The comment width exceeds the shape's width
+        // 4) No URLS were found in the commnet
+        let should_wrap_comment = self.fmt.config.wrap_comments()
+            && !is_markdown_header_doc_comment
             && unicode_str_width(line) > self.fmt.shape.width
-            && !has_url(line)
-        {
+            && !has_url(line);
+
+        if should_wrap_comment {
             match rewrite_string(line, &self.fmt, self.max_width) {
                 Some(ref s) => {
                     self.is_prev_line_multi_line = s.contains('\n');
@@ -882,7 +892,7 @@ fn rewrite_comment_inner(
         });
 
     for (i, (line, has_leading_whitespace)) in lines.enumerate() {
-        if rewriter.handle_line(orig, i, line, has_leading_whitespace) {
+        if rewriter.handle_line(orig, i, line, has_leading_whitespace, is_doc_comment) {
             break;
         }
     }
diff --git a/src/parse/session.rs b/src/parse/session.rs
index 624fed0d..fb918215 100644
--- a/src/parse/session.rs
+++ b/src/parse/session.rs
@@ -12,6 +12,7 @@ use rustc_span::{
 
 use crate::config::file_lines::LineRange;
 use crate::ignore_path::IgnorePathSet;
+use crate::parse::parser::{ModError, ModulePathSuccess};
 use crate::source_map::LineRangeUtils;
 use crate::utils::starts_with_newline;
 use crate::visitor::SnippetProvider;
@@ -145,13 +146,30 @@ impl ParseSess {
         })
     }
 
+    /// Determine the submodule path for the given module identifier.
+    ///
+    /// * `id` - The name of the module
+    /// * `relative` - If Some(symbol), the symbol name is a directory relative to the dir_path.
+    ///   If relative is Some, resolve the submodle at {dir_path}/{symbol}/{id}.rs
+    ///   or {dir_path}/{symbol}/{id}/mod.rs. if None, resolve the module at {dir_path}/{id}.rs.
+    /// *  `dir_path` - Module resolution will occur relative to this direcotry.
     pub(crate) fn default_submod_path(
         &self,
         id: symbol::Ident,
         relative: Option<symbol::Ident>,
         dir_path: &Path,
-    ) -> Result<rustc_expand::module::ModulePathSuccess, rustc_expand::module::ModError<'_>> {
-        rustc_expand::module::default_submod_path(&self.parse_sess, id, relative, dir_path)
+    ) -> Result<ModulePathSuccess, ModError<'_>> {
+        rustc_expand::module::default_submod_path(&self.parse_sess, id, relative, dir_path).or_else(
+            |e| {
+                // If resloving a module relative to {dir_path}/{symbol} fails because a file
+                // could not be found, then try to resolve the module relative to {dir_path}.
+                if matches!(e, ModError::FileNotFound(..)) && relative.is_some() {
+                    rustc_expand::module::default_submod_path(&self.parse_sess, id, None, dir_path)
+                } else {
+                    Err(e)
+                }
+            },
+        )
     }
 
     pub(crate) fn is_file_parsed(&self, path: &Path) -> bool {


Test patch:
diff --git a/src/test/mod_resolver.rs b/src/test/mod_resolver.rs
index fcff6d14..aacb2acc 100644
--- a/src/test/mod_resolver.rs
+++ b/src/test/mod_resolver.rs
@@ -64,3 +64,19 @@ fn fmt_out_of_line_test_modules() {
         ],
     )
 }
+
+#[test]
+fn fallback_and_try_to_resolve_external_submod_relative_to_current_dir_path() {
+    // See also https://github.com/rust-lang/rustfmt/issues/5198
+    verify_mod_resolution(
+        "tests/mod-resolver/issue-5198/lib.rs",
+        &[
+            "tests/mod-resolver/issue-5198/a.rs",
+            "tests/mod-resolver/issue-5198/lib/b.rs",
+            "tests/mod-resolver/issue-5198/lib/c/mod.rs",
+            "tests/mod-resolver/issue-5198/lib/c/e.rs",
+            "tests/mod-resolver/issue-5198/lib/c/d/f.rs",
+            "tests/mod-resolver/issue-5198/lib/c/d/g/mod.rs",
+        ],
+    )
+}
diff --git a/tests/mod-resolver/issue-5198/a.rs b/tests/mod-resolver/issue-5198/a.rs
new file mode 100644
index 00000000..cd686f56
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/a.rs
@@ -0,0 +1,1 @@
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib.rs b/tests/mod-resolver/issue-5198/lib.rs
new file mode 100644
index 00000000..69683291
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib.rs
@@ -0,0 +1,3 @@
+mod a;
+mod b;
+mod c;
diff --git a/tests/mod-resolver/issue-5198/lib/b.rs b/tests/mod-resolver/issue-5198/lib/b.rs
new file mode 100644
index 00000000..cd686f56
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/b.rs
@@ -0,0 +1,1 @@
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib/c/d.rs b/tests/mod-resolver/issue-5198/lib/c/d.rs
new file mode 100644
index 00000000..d1604aa2
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/d.rs
@@ -0,0 +1,3 @@
+mod e;
+mod f;
+mod g;
diff --git a/tests/mod-resolver/issue-5198/lib/c/d/explanation.txt b/tests/mod-resolver/issue-5198/lib/c/d/explanation.txt
new file mode 100644
index 00000000..92c9e302
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/d/explanation.txt
@@ -0,0 +1,16 @@
+This file is contained in the './lib/c/d/' directory.
+
+The directory name './lib/c/d/' conflicts with the './lib/c/d.rs' file name.
+
+'./lib/c/d.rs' defines 3 external modules:
+
+    * mod e;
+    * mod f;
+    * mod g;
+
+Module resolution will fail if we look for './lib/c/d/e.rs' or './lib/c/d/e/mod.rs',
+so we should fall back to looking for './lib/c/e.rs', which correctly finds the modlue, that
+rustfmt should format.
+
+'./lib/c/d/f.rs' and './lib/c/d/g/mod.rs' exist at the default submodule paths so we should be able
+to resolve these modules with no problems.
\ No newline at end of file
diff --git a/tests/mod-resolver/issue-5198/lib/c/d/f.rs b/tests/mod-resolver/issue-5198/lib/c/d/f.rs
new file mode 100644
index 00000000..cd686f56
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/d/f.rs
@@ -0,0 +1,1 @@
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib/c/d/g/mod.rs b/tests/mod-resolver/issue-5198/lib/c/d/g/mod.rs
new file mode 100644
index 00000000..cd686f56
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/d/g/mod.rs
@@ -0,0 +1,1 @@
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib/c/e.rs b/tests/mod-resolver/issue-5198/lib/c/e.rs
new file mode 100644
index 00000000..cd686f56
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/e.rs
@@ -0,0 +1,1 @@
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib/c/mod.rs b/tests/mod-resolver/issue-5198/lib/c/mod.rs
new file mode 100644
index 00000000..81904619
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/c/mod.rs
@@ -0,0 +1,3 @@
+mod d;
+
+fn main(        ) {   println!("Hello World!")                         }
diff --git a/tests/mod-resolver/issue-5198/lib/explanation.txt b/tests/mod-resolver/issue-5198/lib/explanation.txt
new file mode 100644
index 00000000..d436a807
--- /dev/null
+++ b/tests/mod-resolver/issue-5198/lib/explanation.txt
@@ -0,0 +1,16 @@
+This file is contained in the './lib' directory.
+
+The directory name './lib' conflicts with the './lib.rs' file name.
+
+'lib.rs' defines 3 external modules:
+
+    * mod a;
+    * mod b;
+    * mod c;
+
+Module resolution will fail if we look for './lib/a.rs' or './lib/a/mod.rs',
+so we should fall back to looking for './a.rs', which correctly finds the modlue that
+rustfmt should format.
+
+'./lib/b.rs' and './lib/c/mod.rs' exist at the default submodule paths so we should be able
+to resolve these modules with no problems.
diff --git a/tests/source/issue-5238/markdown_header_wrap_comments_false.rs b/tests/source/issue-5238/markdown_header_wrap_comments_false.rs
new file mode 100644
index 00000000..229c6e57
--- /dev/null
+++ b/tests/source/issue-5238/markdown_header_wrap_comments_false.rs
@@ -0,0 +1,11 @@
+// rustfmt-wrap_comments: false
+
+/// no markdown header so rustfmt should wrap this comment when `format_code_in_doc_comments = true` and `wrap_comments = true`
+fn not_documented_with_markdown_header() {
+    // This is just a normal inline comment so rustfmt should wrap this comment when `wrap_comments = true`
+}
+
+/// # We're using a markdown header here so rustfmt should refuse to wrap this comment in all circumstances
+fn documented_with_markdown_header() {
+    // # We're using a markdown header in an inline comment. rustfmt should be able to wrap this comment when `wrap_comments = true`
+}
diff --git a/tests/source/issue-5238/markdown_header_wrap_comments_true.rs b/tests/source/issue-5238/markdown_header_wrap_comments_true.rs
new file mode 100644
index 00000000..c547ff35
--- /dev/null
+++ b/tests/source/issue-5238/markdown_header_wrap_comments_true.rs
@@ -0,0 +1,11 @@
+// rustfmt-wrap_comments: true
+
+/// no markdown header so rustfmt should wrap this comment when `format_code_in_doc_comments = true` and `wrap_comments = true`
+fn not_documented_with_markdown_header() {
+    // This is just a normal inline comment so rustfmt should wrap this comment when `wrap_comments = true`
+}
+
+/// # We're using a markdown header here so rustfmt should refuse to wrap this comment in all circumstances
+fn documented_with_markdown_header() {
+    // # We're using a markdown header in an inline comment. rustfmt should be able to wrap this comment when `wrap_comments = true`
+}
diff --git a/tests/target/issue-5238/markdown_header_wrap_comments_false.rs b/tests/target/issue-5238/markdown_header_wrap_comments_false.rs
new file mode 100644
index 00000000..229c6e57
--- /dev/null
+++ b/tests/target/issue-5238/markdown_header_wrap_comments_false.rs
@@ -0,0 +1,11 @@
+// rustfmt-wrap_comments: false
+
+/// no markdown header so rustfmt should wrap this comment when `format_code_in_doc_comments = true` and `wrap_comments = true`
+fn not_documented_with_markdown_header() {
+    // This is just a normal inline comment so rustfmt should wrap this comment when `wrap_comments = true`
+}
+
+/// # We're using a markdown header here so rustfmt should refuse to wrap this comment in all circumstances
+fn documented_with_markdown_header() {
+    // # We're using a markdown header in an inline comment. rustfmt should be able to wrap this comment when `wrap_comments = true`
+}
diff --git a/tests/target/issue-5238/markdown_header_wrap_comments_true.rs b/tests/target/issue-5238/markdown_header_wrap_comments_true.rs
new file mode 100644
index 00000000..87dae58e
--- /dev/null
+++ b/tests/target/issue-5238/markdown_header_wrap_comments_true.rs
@@ -0,0 +1,14 @@
+// rustfmt-wrap_comments: true
+
+/// no markdown header so rustfmt should wrap this comment when
+/// `format_code_in_doc_comments = true` and `wrap_comments = true`
+fn not_documented_with_markdown_header() {
+    // This is just a normal inline comment so rustfmt should wrap this comment
+    // when `wrap_comments = true`
+}
+
+/// # We're using a markdown header here so rustfmt should refuse to wrap this comment in all circumstances
+fn documented_with_markdown_header() {
+    // # We're using a markdown header in an inline comment. rustfmt should be
+    // able to wrap this comment when `wrap_comments = true`
+}