about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Howell <michael@notriddle.com>2024-06-28 11:00:42 -0700
committerblyxyas <blyxyas@gmail.com>2024-07-02 23:27:14 +0200
commit70c8579e210d2fc8dae3bd0ae14c2210326eab5f (patch)
treeb85e0bd2d97c8de51de0b734ec2aeccd0fb82f42
parent2f80536e8353f7fbbfe72725d25a7feada3d2eb3 (diff)
downloadrust-70c8579e210d2fc8dae3bd0ae14c2210326eab5f.tar.gz
rust-70c8579e210d2fc8dae3bd0ae14c2210326eab5f.zip
doc_markdown: detect escaped `` ` `` when checking unmatched
Add explanatory comment to complex bounds check

Format
-rw-r--r--clippy_lints/src/doc/mod.rs13
-rw-r--r--tests/ui/doc/unbalanced_ticks.rs17
-rw-r--r--tests/ui/doc/unbalanced_ticks.stderr18
3 files changed, 46 insertions, 2 deletions
diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs
index 5faa00b7c97..357ed08c7c7 100644
--- a/clippy_lints/src/doc/mod.rs
+++ b/clippy_lints/src/doc/mod.rs
@@ -769,7 +769,18 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
             TaskListMarker(_) | Code(_) | Rule => (),
             FootnoteReference(text) | Text(text) => {
                 paragraph_range.end = range.end;
-                ticks_unbalanced |= text.contains('`') && !in_code;
+                let range_ = range.clone();
+                ticks_unbalanced |= text.contains('`')
+                    && !in_code
+                    && doc[range.clone()].bytes().enumerate().any(|(i, c)| {
+                        // scan the markdown source code bytes for backquotes that aren't preceded by backslashes
+                        // - use bytes, instead of chars, to avoid utf8 decoding overhead (special chars are ascii)
+                        // - relevant backquotes are within doc[range], but backslashes are not, because they're not
+                        //   actually part of the rendered text (pulldown-cmark doesn't emit any events for escapes)
+                        // - if `range_.start + i == 0`, then `range_.start + i - 1 == -1`, and since we're working in
+                        //   usize, that would underflow and maybe panic
+                        c == b'`' && (range_.start + i == 0 || doc.as_bytes().get(range_.start + i - 1) != Some(&b'\\'))
+                    });
                 if Some(&text) == in_link.as_ref() || ticks_unbalanced {
                     // Probably a link of the form `<http://example.com>`
                     // Which are represented as a link to "http://example.com" with
diff --git a/tests/ui/doc/unbalanced_ticks.rs b/tests/ui/doc/unbalanced_ticks.rs
index 6f7bab72040..04446787b6c 100644
--- a/tests/ui/doc/unbalanced_ticks.rs
+++ b/tests/ui/doc/unbalanced_ticks.rs
@@ -49,3 +49,20 @@ fn other_markdown() {}
 ///   pub struct Struct;
 ///   ```
 fn issue_7421() {}
+
+/// `
+//~^ ERROR: backticks are unbalanced
+fn escape_0() {}
+
+/// Escaped \` backticks don't count.
+fn escape_1() {}
+
+/// Escaped \` \` backticks don't count.
+fn escape_2() {}
+
+/// Escaped \` ` backticks don't count, but unescaped backticks do.
+//~^ ERROR: backticks are unbalanced
+fn escape_3() {}
+
+/// Backslashes ` \` within code blocks don't count.
+fn escape_4() {}
diff --git a/tests/ui/doc/unbalanced_ticks.stderr b/tests/ui/doc/unbalanced_ticks.stderr
index 56ef2913623..50324010e97 100644
--- a/tests/ui/doc/unbalanced_ticks.stderr
+++ b/tests/ui/doc/unbalanced_ticks.stderr
@@ -78,5 +78,21 @@ help: try
 LL | /// - This item needs `backticks_here`
    |                       ~~~~~~~~~~~~~~~~
 
-error: aborting due to 8 previous errors
+error: backticks are unbalanced
+  --> tests/ui/doc/unbalanced_ticks.rs:53:5
+   |
+LL | /// `
+   |     ^
+   |
+   = help: a backtick may be missing a pair
+
+error: backticks are unbalanced
+  --> tests/ui/doc/unbalanced_ticks.rs:63:5
+   |
+LL | /// Escaped \` ` backticks don't count, but unescaped backticks do.
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: a backtick may be missing a pair
+
+error: aborting due to 10 previous errors