about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/doc/rustdoc/src/lints.md38
-rw-r--r--src/librustdoc/lib.rs11
-rw-r--r--src/librustdoc/lint.rs12
-rw-r--r--src/librustdoc/passes/lint.rs2
-rw-r--r--src/librustdoc/passes/lint/unescaped_backticks.rs416
-rw-r--r--tests/rustdoc-ui/unescaped_backticks.rs342
-rw-r--r--tests/rustdoc-ui/unescaped_backticks.stderr959
7 files changed, 1775 insertions, 5 deletions
diff --git a/src/doc/rustdoc/src/lints.md b/src/doc/rustdoc/src/lints.md
index 45db3bb9b00..fd57b079644 100644
--- a/src/doc/rustdoc/src/lints.md
+++ b/src/doc/rustdoc/src/lints.md
@@ -374,3 +374,41 @@ warning: this URL is not a hyperlink
 
 warning: 2 warnings emitted
 ```
+
+## `unescaped_backticks`
+
+This lint is **allowed by default**. It detects backticks (\`) that are not escaped.
+This usually means broken inline code. For example:
+
+```rust
+#![warn(rustdoc::unescaped_backticks)]
+
+/// `add(a, b) is the same as `add(b, a)`.
+pub fn add(a: i32, b: i32) -> i32 { a + b }
+```
+
+Which will give:
+
+```text
+warning: unescaped backtick
+ --> src/lib.rs:3:41
+  |
+3 | /// `add(a, b) is the same as `add(b, a)`.
+  |                                         ^
+  |
+note: the lint level is defined here
+ --> src/lib.rs:1:9
+  |
+1 | #![warn(rustdoc::unescaped_backticks)]
+  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+help: a previous inline code might be longer than expected
+  |
+3 | /// `add(a, b)` is the same as `add(b, a)`.
+  |               +
+help: if you meant to use a literal backtick, escape it
+  |
+3 | /// `add(a, b) is the same as `add(b, a)\`.
+  |                                         +
+
+warning: 1 warning emitted
+```
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index c15afca2261..60754130d99 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -7,14 +7,15 @@
 #![feature(assert_matches)]
 #![feature(box_patterns)]
 #![feature(drain_filter)]
+#![feature(impl_trait_in_assoc_type)]
+#![feature(iter_intersperse)]
+#![feature(lazy_cell)]
 #![feature(let_chains)]
-#![feature(test)]
 #![feature(never_type)]
-#![feature(lazy_cell)]
-#![feature(type_ascription)]
-#![feature(iter_intersperse)]
+#![feature(round_char_boundary)]
+#![feature(test)]
 #![feature(type_alias_impl_trait)]
-#![feature(impl_trait_in_assoc_type)]
+#![feature(type_ascription)]
 #![recursion_limit = "256"]
 #![warn(rustc::internal)]
 #![allow(clippy::collapsible_if, clippy::collapsible_else_if)]
diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs
index 6d289eb996d..749c1ff51bf 100644
--- a/src/librustdoc/lint.rs
+++ b/src/librustdoc/lint.rs
@@ -174,6 +174,17 @@ declare_rustdoc_lint! {
    "codeblock could not be parsed as valid Rust or is empty"
 }
 
+declare_rustdoc_lint! {
+   /// The `unescaped_backticks` lint detects unescaped backticks (\`), which usually
+   /// mean broken inline code. This is a `rustdoc` only lint, see the documentation
+   /// in the [rustdoc book].
+   ///
+   /// [rustdoc book]: ../../../rustdoc/lints.html#unescaped_backticks
+   UNESCAPED_BACKTICKS,
+   Allow,
+   "detects unescaped backticks in doc comments"
+}
+
 pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
     vec![
         BROKEN_INTRA_DOC_LINKS,
@@ -185,6 +196,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
         INVALID_HTML_TAGS,
         BARE_URLS,
         MISSING_CRATE_LEVEL_DOCS,
+        UNESCAPED_BACKTICKS,
     ]
 });
 
diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs
index 97031c4f028..e653207b9b6 100644
--- a/src/librustdoc/passes/lint.rs
+++ b/src/librustdoc/passes/lint.rs
@@ -4,6 +4,7 @@
 mod bare_urls;
 mod check_code_block_syntax;
 mod html_tags;
+mod unescaped_backticks;
 
 use super::Pass;
 use crate::clean::*;
@@ -27,6 +28,7 @@ impl<'a, 'tcx> DocVisitor for Linter<'a, 'tcx> {
         bare_urls::visit_item(self.cx, item);
         check_code_block_syntax::visit_item(self.cx, item);
         html_tags::visit_item(self.cx, item);
+        unescaped_backticks::visit_item(self.cx, item);
 
         self.visit_item_recur(item)
     }
diff --git a/src/librustdoc/passes/lint/unescaped_backticks.rs b/src/librustdoc/passes/lint/unescaped_backticks.rs
new file mode 100644
index 00000000000..33cef82a60c
--- /dev/null
+++ b/src/librustdoc/passes/lint/unescaped_backticks.rs
@@ -0,0 +1,416 @@
+//! Detects unescaped backticks (\`) in doc comments.
+
+use crate::clean::Item;
+use crate::core::DocContext;
+use crate::html::markdown::main_body_opts;
+use crate::passes::source_span_for_markdown_range;
+use pulldown_cmark::{BrokenLink, Event, Parser};
+use rustc_errors::DiagnosticBuilder;
+use rustc_lint_defs::Applicability;
+use std::ops::Range;
+
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
+    let tcx = cx.tcx;
+    let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {
+        // If non-local, no need to check anything.
+        return;
+    };
+
+    let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
+    if dox.is_empty() {
+        return;
+    }
+
+    let link_names = item.link_names(&cx.cache);
+    let mut replacer = |broken_link: BrokenLink<'_>| {
+        link_names
+            .iter()
+            .find(|link| *link.original_text == *broken_link.reference)
+            .map(|link| ((*link.href).into(), (*link.new_text).into()))
+    };
+    let parser = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer))
+        .into_offset_iter();
+
+    let mut element_stack = Vec::new();
+
+    let mut prev_text_end = 0;
+    for (event, event_range) in parser {
+        match event {
+            Event::Start(_) => {
+                element_stack.push(Element::new(event_range));
+            }
+            Event::End(_) => {
+                let element = element_stack.pop().unwrap();
+
+                let Some(backtick_index) = element.backtick_index else {
+                    continue;
+                };
+
+                // If we can't get a span of the backtick, because it is in a `#[doc = ""]` attribute,
+                // use the span of the entire attribute as a fallback.
+                let span = source_span_for_markdown_range(
+                    tcx,
+                    &dox,
+                    &(backtick_index..backtick_index + 1),
+                    &item.attrs,
+                )
+                .unwrap_or_else(|| item.attr_span(tcx));
+
+                cx.tcx.struct_span_lint_hir(crate::lint::UNESCAPED_BACKTICKS, hir_id, span, "unescaped backtick", |lint| {
+                    let mut help_emitted = false;
+
+                    match element.prev_code_guess {
+                        PrevCodeGuess::None => {}
+                        PrevCodeGuess::Start { guess, .. } => {
+                            // "foo` `bar`" -> "`foo` `bar`"
+                            if let Some(suggest_index) = clamp_start(guess, &element.suggestible_ranges)
+                                && can_suggest_backtick(&dox, suggest_index)
+                            {
+                                suggest_insertion(cx, item, &dox, lint, suggest_index, '`', "the opening backtick of a previous inline code may be missing");
+                                help_emitted = true;
+                            }
+                        }
+                        PrevCodeGuess::End { guess, .. } => {
+                            // "`foo `bar`" -> "`foo` `bar`"
+                            // Don't `clamp_end` here, because the suggestion is guaranteed to be inside
+                            // an inline code node and we intentionally "break" the inline code here.
+                            let suggest_index = guess;
+                            if can_suggest_backtick(&dox, suggest_index) {
+                                suggest_insertion(cx, item, &dox, lint, suggest_index, '`', "a previous inline code might be longer than expected");
+                                help_emitted = true;
+                            }
+                        }
+                    }
+
+                    if !element.prev_code_guess.is_confident() {
+                        // "`foo` bar`" -> "`foo` `bar`"
+                        if let Some(guess) = guess_start_of_code(&dox, element.element_range.start..backtick_index)
+                            && let Some(suggest_index) = clamp_start(guess, &element.suggestible_ranges)
+                            && can_suggest_backtick(&dox, suggest_index)
+                        {
+                            suggest_insertion(cx, item, &dox, lint, suggest_index, '`', "the opening backtick of an inline code may be missing");
+                            help_emitted = true;
+                        }
+
+                        // "`foo` `bar" -> "`foo` `bar`"
+                        // Don't suggest closing backtick after single trailing char,
+                        // if we already suggested opening backtick. For example:
+                        // "foo`." -> "`foo`." or "foo`s" -> "`foo`s".
+                        if let Some(guess) = guess_end_of_code(&dox, backtick_index + 1..element.element_range.end)
+                            && let Some(suggest_index) = clamp_end(guess, &element.suggestible_ranges)
+                            && can_suggest_backtick(&dox, suggest_index)
+                            && (!help_emitted || suggest_index - backtick_index > 2)
+                        {
+                            suggest_insertion(cx, item, &dox, lint, suggest_index, '`', "the closing backtick of an inline code may be missing");
+                            help_emitted = true;
+                        }
+                    }
+
+                    if !help_emitted {
+                        lint.help("the opening or closing backtick of an inline code may be missing");
+                    }
+
+                    suggest_insertion(cx, item, &dox, lint, backtick_index, '\\', "if you meant to use a literal backtick, escape it");
+
+                    lint
+                });
+            }
+            Event::Code(_) => {
+                let element = element_stack
+                    .last_mut()
+                    .expect("expected inline code node to be inside of an element");
+                assert!(
+                    event_range.start >= element.element_range.start
+                        && event_range.end <= element.element_range.end
+                );
+
+                // This inline code might be longer than it's supposed to be.
+                // Only check single backtick inline code for now.
+                if !element.prev_code_guess.is_confident()
+                    && dox.as_bytes().get(event_range.start) == Some(&b'`')
+                    && dox.as_bytes().get(event_range.start + 1) != Some(&b'`')
+                {
+                    let range_inside = event_range.start + 1..event_range.end - 1;
+                    let text_inside = &dox[range_inside.clone()];
+
+                    let is_confident = text_inside.starts_with(char::is_whitespace)
+                        || text_inside.ends_with(char::is_whitespace);
+
+                    if let Some(guess) = guess_end_of_code(&dox, range_inside) {
+                        // Find earlier end of code.
+                        element.prev_code_guess = PrevCodeGuess::End { guess, is_confident };
+                    } else {
+                        // Find alternate start of code.
+                        let range_before = element.element_range.start..event_range.start;
+                        if let Some(guess) = guess_start_of_code(&dox, range_before) {
+                            element.prev_code_guess = PrevCodeGuess::Start { guess, is_confident };
+                        }
+                    }
+                }
+            }
+            Event::Text(text) => {
+                let element = element_stack
+                    .last_mut()
+                    .expect("expected inline text node to be inside of an element");
+                assert!(
+                    event_range.start >= element.element_range.start
+                        && event_range.end <= element.element_range.end
+                );
+
+                // The first char is escaped if the prev char is \ and not part of a text node.
+                let is_escaped = prev_text_end < event_range.start
+                    && dox.as_bytes()[event_range.start - 1] == b'\\';
+
+                // Don't lint backslash-escaped (\`) or html-escaped (&#96;) backticks.
+                if *text == *"`" && !is_escaped && *text == dox[event_range.clone()] {
+                    // We found a stray backtick.
+                    assert!(
+                        element.backtick_index.is_none(),
+                        "expected at most one unescaped backtick per element",
+                    );
+                    element.backtick_index = Some(event_range.start);
+                }
+
+                prev_text_end = event_range.end;
+
+                if is_escaped {
+                    // Ensure that we suggest "`\x" and not "\`x".
+                    element.suggestible_ranges.push(event_range.start - 1..event_range.end);
+                } else {
+                    element.suggestible_ranges.push(event_range);
+                }
+            }
+            _ => {}
+        }
+    }
+}
+
+/// A previous inline code node, that looks wrong.
+///
+/// `guess` is the position, where we want to suggest a \` and the guess `is_confident` if an
+/// inline code starts or ends with a whitespace.
+#[derive(Debug)]
+enum PrevCodeGuess {
+    None,
+
+    /// Missing \` at start.
+    ///
+    /// ```markdown
+    /// foo` `bar`
+    /// ```
+    Start {
+        guess: usize,
+        is_confident: bool,
+    },
+
+    /// Missing \` at end.
+    ///
+    /// ```markdown
+    /// `foo `bar`
+    /// ```
+    End {
+        guess: usize,
+        is_confident: bool,
+    },
+}
+
+impl PrevCodeGuess {
+    fn is_confident(&self) -> bool {
+        match *self {
+            PrevCodeGuess::None => false,
+            PrevCodeGuess::Start { is_confident, .. } | PrevCodeGuess::End { is_confident, .. } => {
+                is_confident
+            }
+        }
+    }
+}
+
+/// A markdown [tagged element], which may or may not contain an unescaped backtick.
+///
+/// [tagged element]: https://docs.rs/pulldown-cmark/0.9/pulldown_cmark/enum.Tag.html
+#[derive(Debug)]
+struct Element {
+    /// The full range (span) of the element in the doc string.
+    element_range: Range<usize>,
+
+    /// The ranges where we're allowed to put backticks.
+    /// This is used to prevent breaking markdown elements like links or lists.
+    suggestible_ranges: Vec<Range<usize>>,
+
+    /// The unescaped backtick.
+    backtick_index: Option<usize>,
+
+    /// Suggest a different start or end of an inline code.
+    prev_code_guess: PrevCodeGuess,
+}
+
+impl Element {
+    const fn new(element_range: Range<usize>) -> Self {
+        Self {
+            element_range,
+            suggestible_ranges: Vec::new(),
+            backtick_index: None,
+            prev_code_guess: PrevCodeGuess::None,
+        }
+    }
+}
+
+/// Given a potentially unclosed inline code, attempt to find the start.
+fn guess_start_of_code(dox: &str, range: Range<usize>) -> Option<usize> {
+    assert!(dox.as_bytes()[range.end] == b'`');
+
+    let mut braces = 0;
+    let mut guess = 0;
+    for (idx, ch) in dox[range.clone()].char_indices().rev() {
+        match ch {
+            ')' | ']' | '}' => braces += 1,
+            '(' | '[' | '{' => {
+                if braces == 0 {
+                    guess = idx + 1;
+                    break;
+                }
+                braces -= 1;
+            }
+            ch if ch.is_whitespace() && braces == 0 => {
+                guess = idx + 1;
+                break;
+            }
+            _ => (),
+        }
+    }
+
+    guess += range.start;
+
+    // Don't suggest empty inline code or duplicate backticks.
+    can_suggest_backtick(dox, guess).then_some(guess)
+}
+
+/// Given a potentially unclosed inline code, attempt to find the end.
+fn guess_end_of_code(dox: &str, range: Range<usize>) -> Option<usize> {
+    // Punctuation that should be outside of the inline code.
+    const TRAILING_PUNCTUATION: &[u8] = b".,";
+
+    assert!(dox.as_bytes()[range.start - 1] == b'`');
+
+    let text = dox[range.clone()].trim_end();
+    let mut braces = 0;
+    let mut guess = text.len();
+    for (idx, ch) in text.char_indices() {
+        match ch {
+            '(' | '[' | '{' => braces += 1,
+            ')' | ']' | '}' => {
+                if braces == 0 {
+                    guess = idx;
+                    break;
+                }
+                braces -= 1;
+            }
+            ch if ch.is_whitespace() && braces == 0 => {
+                guess = idx;
+                break;
+            }
+            _ => (),
+        }
+    }
+
+    // Strip a single trailing punctuation.
+    if guess >= 1
+        && TRAILING_PUNCTUATION.contains(&text.as_bytes()[guess - 1])
+        && (guess < 2 || !TRAILING_PUNCTUATION.contains(&text.as_bytes()[guess - 2]))
+    {
+        guess -= 1;
+    }
+
+    guess += range.start;
+
+    // Don't suggest empty inline code or duplicate backticks.
+    can_suggest_backtick(dox, guess).then_some(guess)
+}
+
+/// Returns whether inserting a backtick at `dox[index]` will not produce double backticks.
+fn can_suggest_backtick(dox: &str, index: usize) -> bool {
+    (index == 0 || dox.as_bytes()[index - 1] != b'`')
+        && (index == dox.len() || dox.as_bytes()[index] != b'`')
+}
+
+/// Increase the index until it is inside or one past the end of one of the ranges.
+///
+/// The ranges must be sorted for this to work correctly.
+fn clamp_start(index: usize, ranges: &[Range<usize>]) -> Option<usize> {
+    for range in ranges {
+        if range.start >= index {
+            return Some(range.start);
+        }
+        if index <= range.end {
+            return Some(index);
+        }
+    }
+    None
+}
+
+/// Decrease the index until it is inside or one past the end of one of the ranges.
+///
+/// The ranges must be sorted for this to work correctly.
+fn clamp_end(index: usize, ranges: &[Range<usize>]) -> Option<usize> {
+    for range in ranges.iter().rev() {
+        if range.end <= index {
+            return Some(range.end);
+        }
+        if index >= range.start {
+            return Some(index);
+        }
+    }
+    None
+}
+
+/// Try to emit a span suggestion and fall back to help messages if we can't find a suitable span.
+///
+/// This helps finding backticks in huge macro-generated docs.
+fn suggest_insertion(
+    cx: &DocContext<'_>,
+    item: &Item,
+    dox: &str,
+    lint: &mut DiagnosticBuilder<'_, ()>,
+    insert_index: usize,
+    suggestion: char,
+    message: &str,
+) {
+    /// Maximum bytes of context to show around the insertion.
+    const CONTEXT_MAX_LEN: usize = 80;
+
+    if let Some(span) =
+        source_span_for_markdown_range(cx.tcx, &dox, &(insert_index..insert_index), &item.attrs)
+    {
+        lint.span_suggestion(span, message, suggestion, Applicability::MaybeIncorrect);
+    } else {
+        let line_start = dox[..insert_index].rfind('\n').map_or(0, |idx| idx + 1);
+        let line_end = dox[insert_index..].find('\n').map_or(dox.len(), |idx| idx + insert_index);
+
+        let context_before_max_len = if insert_index - line_start < CONTEXT_MAX_LEN / 2 {
+            insert_index - line_start
+        } else if line_end - insert_index < CONTEXT_MAX_LEN / 2 {
+            CONTEXT_MAX_LEN - (line_end - insert_index)
+        } else {
+            CONTEXT_MAX_LEN / 2
+        };
+        let context_after_max_len = CONTEXT_MAX_LEN - context_before_max_len;
+
+        let (prefix, context_start) = if insert_index - line_start <= context_before_max_len {
+            ("", line_start)
+        } else {
+            ("...", dox.ceil_char_boundary(insert_index - context_before_max_len))
+        };
+        let (suffix, context_end) = if line_end - insert_index <= context_after_max_len {
+            ("", line_end)
+        } else {
+            ("...", dox.floor_char_boundary(insert_index + context_after_max_len))
+        };
+
+        let context_full = &dox[context_start..context_end].trim_end();
+        let context_before = &dox[context_start..insert_index];
+        let context_after = &dox[insert_index..context_end].trim_end();
+        lint.help(format!(
+            "{message}\n change: {prefix}{context_full}{suffix}\nto this: {prefix}{context_before}{suggestion}{context_after}{suffix}"
+        ));
+    }
+}
diff --git a/tests/rustdoc-ui/unescaped_backticks.rs b/tests/rustdoc-ui/unescaped_backticks.rs
new file mode 100644
index 00000000000..f1ad7c8d4c7
--- /dev/null
+++ b/tests/rustdoc-ui/unescaped_backticks.rs
@@ -0,0 +1,342 @@
+#![deny(rustdoc::unescaped_backticks)]
+#![allow(rustdoc::broken_intra_doc_links)]
+#![allow(rustdoc::invalid_html_tags)]
+
+///
+pub fn empty() {}
+
+#[doc = ""]
+pub fn empty2() {}
+
+/// `
+//~^ ERROR unescaped backtick
+pub fn single() {}
+
+/// \`
+pub fn escaped() {}
+
+/// \\`
+//~^ ERROR unescaped backtick
+pub fn not_escaped() {}
+
+/// \\\`
+pub fn not_not_escaped() {}
+
+/// [`link1]
+//~^ ERROR unescaped backtick
+pub fn link1() {}
+
+/// [link2`]
+//~^ ERROR unescaped backtick
+pub fn link2() {}
+
+/// [`link_long](link_long)
+//~^ ERROR unescaped backtick
+pub fn link_long() {}
+
+/// [`broken-link]
+//~^ ERROR unescaped backtick
+pub fn broken_link() {}
+
+/// <xx:`>
+pub fn url() {}
+
+/// <x:`>
+//~^ ERROR unescaped backtick
+pub fn not_url() {}
+
+/// <h1>`</h1>
+pub fn html_tag() {}
+
+/// &#96;
+pub fn html_escape() {}
+
+/// 🦀`🦀
+//~^ ERROR unescaped backtick
+pub fn unicode() {}
+
+/// `foo(
+//~^ ERROR unescaped backtick
+///
+/// paragraph
+pub fn paragraph() {}
+
+/// `foo `bar`
+//~^ ERROR unescaped backtick
+///
+/// paragraph
+pub fn paragraph2() {}
+
+/// `foo(
+//~^ ERROR unescaped backtick
+/// not paragraph
+pub fn not_paragraph() {}
+
+/// Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`.
+//~^ ERROR unescaped backtick
+///
+/// You could use this function to add 42 to a number `n` (add(n, 42)`),
+/// or even to add a number `n` to 42 (`add(42, b)`)!
+//~^ ERROR unescaped backtick
+pub fn add1(a: i32, b: i32) -> i32 { a + b }
+
+/// Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`.
+//~^ ERROR unescaped backtick
+///
+/// You could use this function to add 42 to a number `n` (`add(n, 42)),
+/// or even to add a number `n` to 42 (`add(42, n)`)!
+//~^ ERROR unescaped backtick
+pub fn add2(a: i32, b: i32) -> i32 { a + b }
+
+/// Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`.
+//~^ ERROR unescaped backtick
+///
+/// You could use this function to add 42 to a number `n` (`add(n, 42)`),
+/// or even to add a number `n` to 42 (add(42, n)`)!
+//~^ ERROR unescaped backtick
+pub fn add3(a: i32, b: i32) -> i32 { a + b }
+
+/// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a).
+//~^ ERROR unescaped backtick
+///
+/// You could use this function to add 42 to a number `n` (`add(n, 42)),
+/// or even to add a number `n` to 42 (`add(42, n)`)!
+//~^ ERROR unescaped backtick
+pub fn add4(a: i32, b: i32) -> i32 { a + b }
+
+#[doc = "`"]
+//~^ ERROR unescaped backtick
+pub fn attr() {}
+
+#[doc = concat!("\\", "`")]
+pub fn attr_escaped() {}
+
+#[doc = concat!("\\\\", "`")]
+//~^ ERROR unescaped backtick
+pub fn attr_not_escaped() {}
+
+#[doc = "Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`."]
+//~^ ERROR unescaped backtick
+pub fn attr_add1(a: i32, b: i32) -> i32 { a + b }
+
+#[doc = "Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`."]
+//~^ ERROR unescaped backtick
+pub fn attr_add2(a: i32, b: i32) -> i32 { a + b }
+
+#[doc = "Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`."]
+//~^ ERROR unescaped backtick
+pub fn attr_add3(a: i32, b: i32) -> i32 { a + b }
+
+#[doc = "Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)."]
+//~^ ERROR unescaped backtick
+pub fn attr_add4(a: i32, b: i32) -> i32 { a + b }
+
+/// ``double backticks``
+/// `foo
+//~^ ERROR unescaped backtick
+pub fn double_backticks() {}
+
+/// # `(heading
+//~^ ERROR unescaped backtick
+/// ## heading2)`
+//~^ ERROR unescaped backtick
+///
+/// multi `(
+//~^ ERROR unescaped backtick
+/// line
+/// ) heading
+/// =
+///
+/// para)`(graph
+//~^ ERROR unescaped backtick
+///
+/// para)`(graph2
+//~^ ERROR unescaped backtick
+///
+/// 1. foo)`
+//~^ ERROR unescaped backtick
+/// 2. `(bar
+//~^ ERROR unescaped backtick
+/// * baz)`
+//~^ ERROR unescaped backtick
+/// * `(quux
+//~^ ERROR unescaped backtick
+///
+/// `#![this_is_actually_an_image(and(not), an = "attribute")]
+//~^ ERROR unescaped backtick
+///
+/// #![this_is_actually_an_image(and(not), an = "attribute")]`
+//~^ ERROR unescaped backtick
+///
+/// [this_is_actually_an_image(and(not), an = "attribute")]: `.png
+///
+/// | `table( | )head` |
+//~^ ERROR unescaped backtick
+//~| ERROR unescaped backtick
+/// |---------|--------|
+/// | table`( | )`body |
+//~^ ERROR unescaped backtick
+//~| ERROR unescaped backtick
+pub fn complicated_markdown() {}
+
+/// The `custom_mir` attribute tells the compiler to treat the function as being custom MIR. This
+/// attribute only works on functions - there is no way to insert custom MIR into the middle of
+/// another function. The `dialect` and `phase` parameters indicate which [version of MIR][dialect
+/// docs] you are inserting here. Generally you'll want to use `#![custom_mir(dialect = "built")]`
+/// if you want your MIR to be modified by the full MIR pipeline, or `#![custom_mir(dialect =
+//~^ ERROR unescaped backtick
+/// "runtime", phase = "optimized")] if you don't.
+pub mod mir {}
+
+pub mod rustc {
+    /// Constructs a `TyKind::Error` type and registers a `delay_span_bug` with the given `msg to
+    //~^ ERROR unescaped backtick
+    /// ensure it gets used.
+    pub fn ty_error_with_message() {}
+
+    pub struct WhereClause {
+        /// `true` if we ate a `where` token: this can happen
+        /// if we parsed no predicates (e.g. `struct Foo where {}
+        /// This allows us to accurately pretty-print
+        /// in `nt_to_tokenstream`
+        //~^ ERROR unescaped backtick
+        pub has_where_token: bool,
+    }
+
+    /// A symbol is an interned or gensymed string. The use of `newtype_index!` means
+    /// that `Option<Symbol>` only takes up 4 bytes, because `newtype_index! reserves
+    //~^ ERROR unescaped backtick
+    /// the last 256 values for tagging purposes.
+    pub struct Symbol();
+
+    /// It is equivalent to `OpenOptions::new()` but allows you to write more
+    /// readable code. Instead of `OpenOptions::new().read(true).open("foo.txt")`
+    /// you can write `File::with_options().read(true).open("foo.txt"). This
+    /// also avoids the need to import `OpenOptions`.
+    //~^ ERROR unescaped backtick
+    pub fn with_options() {}
+
+    /// Subtracts `set from `row`. `set` can be either `BitSet` or
+    /// `HybridBitSet`. Has no effect if `row` does not exist.
+    //~^ ERROR unescaped backtick
+    ///
+    /// Returns true if the row was changed.
+    pub fn subtract_row() {}
+
+    pub mod assert_module_sources {
+        //! The reason that we use `cfg=...` and not `#[cfg_attr]` is so that
+        //! the HIR doesn't change as a result of the annotations, which might
+        //! perturb the reuse results.
+        //!
+        //! `#![rustc_expected_cgu_reuse(module="spike", cfg="rpass2", kind="post-lto")]
+        //~^ ERROR unescaped backtick
+        //! allows for doing a more fine-grained check to see if pre- or post-lto data
+        //! was re-used.
+
+        /// `cfg=...
+        //~^ ERROR unescaped backtick
+        pub fn foo() {}
+
+        /// `cfg=... and not `#[cfg_attr]`
+        //~^ ERROR unescaped backtick
+        pub fn bar() {}
+    }
+
+    /// Conceptually, this is like a `Vec<Vec<RWU>>`. But the number of
+    /// RWU`s can get very large, so it uses a more compact representation.
+    //~^ ERROR unescaped backtick
+    pub struct RWUTable {}
+
+    /// Like [Self::canonicalize_query], but preserves distinct universes. For
+    /// example, canonicalizing `&'?0: Trait<'?1>`, where `'?0` is in `U1` and
+    /// `'?1` is in `U3` would be canonicalized to have ?0` in `U1` and `'?1`
+    /// in `U2`.
+    //~^ ERROR unescaped backtick
+    ///
+    /// This is used for Chalk integration.
+    pub fn canonicalize_query_preserving_universes() {}
+
+    /// Note that we used to return `Error` here, but that was quite
+    /// dubious -- the premise was that an error would *eventually* be
+    /// reported, when the obligation was processed. But in general once
+    /// you see an `Error` you are supposed to be able to assume that an
+    /// error *has been* reported, so that you can take whatever heuristic
+    /// paths you want to take. To make things worse, it was possible for
+    /// cycles to arise, where you basically had a setup like `<MyType<$0>
+    /// as Trait>::Foo == $0`. Here, normalizing `<MyType<$0> as
+    /// Trait>::Foo> to `[type error]` would lead to an obligation of
+    /// `<MyType<[type error]> as Trait>::Foo`. We are supposed to report
+    /// an error for this obligation, but we legitimately should not,
+    /// because it contains `[type error]`. Yuck! (See issue #29857 for
+    //~^ ERROR unescaped backtick
+    /// one case where this arose.)
+    pub fn normalize_to_error() {}
+
+    /// you don't want to cache that `B: AutoTrait` or `A: AutoTrait`
+    /// is `EvaluatedToOk`; this is because they were only considered
+    /// ok on the premise that if `A: AutoTrait` held, but we indeed
+    /// encountered a problem (later on) with `A: AutoTrait. So we
+    /// currently set a flag on the stack node for `B: AutoTrait` (as
+    /// well as the second instance of `A: AutoTrait`) to suppress
+    //~^ ERROR unescaped backtick
+    /// caching.
+    pub struct TraitObligationStack;
+
+    /// Extend `scc` so that it can outlive some placeholder region
+    /// from a universe it can't name; at present, the only way for
+    /// this to be true is if `scc` outlives `'static`. This is
+    /// actually stricter than necessary: ideally, we'd support bounds
+    /// like `for<'a: 'b`>` that might then allow us to approximate
+    /// `'a` with `'b` and not `'static`. But it will have to do for
+    //~^ ERROR unescaped backtick
+    /// now.
+    pub fn add_incompatible_universe(){}
+}
+
+/// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+/// which returns an `Option<Dispatch>`. If all [`Dispatch`] clones that point
+/// at the `Subscriber` have been dropped, [`WeakDispatch::upgrade`] will return
+/// `None`. Otherwise, it will return `Some(Dispatch)`.
+//~^ ERROR unescaped backtick
+///
+/// Returns some reference to this `[`Subscriber`] value if it is of type `T`,
+/// or `None` if it isn't.
+//~^ ERROR unescaped backtick
+///
+/// Called before the filtered [`Layer]'s [`on_event`], to determine if
+/// `on_event` should be called.
+//~^ ERROR unescaped backtick
+///
+/// Therefore, if the `Filter will change the value returned by this
+/// method, it is responsible for ensuring that
+/// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+//~^ ERROR unescaped backtick
+/// level changes.
+pub mod tracing {}
+
+macro_rules! id {
+    ($($tt:tt)*) => { $($tt)* }
+}
+
+id! {
+    /// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+    //~^ ERROR unescaped backtick
+    //~| ERROR unescaped backtick
+    //~| ERROR unescaped backtick
+    //~| ERROR unescaped backtick
+    /// which returns an `Option<Dispatch>`. If all [`Dispatch`] clones that point
+    /// at the `Subscriber` have been dropped, [`WeakDispatch::upgrade`] will return
+    /// `None`. Otherwise, it will return `Some(Dispatch)`.
+    ///
+    /// Returns some reference to this `[`Subscriber`] value if it is of type `T`,
+    /// or `None` if it isn't.
+    ///
+    /// Called before the filtered [`Layer]'s [`on_event`], to determine if
+    /// `on_event` should be called.
+    ///
+    /// Therefore, if the `Filter will change the value returned by this
+    /// method, it is responsible for ensuring that
+    /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+    /// level changes.
+    pub mod tracing_macro {}
+}
diff --git a/tests/rustdoc-ui/unescaped_backticks.stderr b/tests/rustdoc-ui/unescaped_backticks.stderr
new file mode 100644
index 00000000000..e629dbc34e9
--- /dev/null
+++ b/tests/rustdoc-ui/unescaped_backticks.stderr
@@ -0,0 +1,959 @@
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:186:70
+   |
+LL | /// if you want your MIR to be modified by the full MIR pipeline, or `#![custom_mir(dialect =
+   |                                                                      ^
+   |
+note: the lint level is defined here
+  --> $DIR/unescaped_backticks.rs:1:9
+   |
+LL | #![deny(rustdoc::unescaped_backticks)]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// "runtime", phase = "optimized")]` if you don't.
+   |                                     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// if you want your MIR to be modified by the full MIR pipeline, or \`#![custom_mir(dialect =
+   |                                                                      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:231:13
+   |
+LL |         //! `#![rustc_expected_cgu_reuse(module="spike", cfg="rpass2", kind="post-lto")]
+   |             ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL |         //! `#![rustc_expected_cgu_reuse(module="spike", cfg="rpass2", kind="post-lto")]`
+   |                                                                                         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |         //! \`#![rustc_expected_cgu_reuse(module="spike", cfg="rpass2", kind="post-lto")]
+   |             +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:236:13
+   |
+LL |         /// `cfg=...
+   |             ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL |         /// `cfg=...`
+   |                     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |         /// \`cfg=...
+   |             +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:240:42
+   |
+LL |         /// `cfg=... and not `#[cfg_attr]`
+   |                                          ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |         /// `cfg=...` and not `#[cfg_attr]`
+   |                     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |         /// `cfg=... and not `#[cfg_attr]\`
+   |                                          +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:192:91
+   |
+LL |     /// Constructs a `TyKind::Error` type and registers a `delay_span_bug` with the given `msg to
+   |                                                                                           ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL |     /// Constructs a `TyKind::Error` type and registers a `delay_span_bug` with the given `msg` to
+   |                                                                                               +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// Constructs a `TyKind::Error` type and registers a `delay_span_bug` with the given \`msg to
+   |                                                                                           +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:201:34
+   |
+LL |         /// in `nt_to_tokenstream`
+   |                                  ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |         /// if we parsed no predicates (e.g. `struct` Foo where {}
+   |                                                     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |         /// in `nt_to_tokenstream\`
+   |                                  +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:207:62
+   |
+LL |     /// that `Option<Symbol>` only takes up 4 bytes, because `newtype_index! reserves
+   |                                                              ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL |     /// that `Option<Symbol>` only takes up 4 bytes, because `newtype_index!` reserves
+   |                                                                             +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// that `Option<Symbol>` only takes up 4 bytes, because \`newtype_index! reserves
+   |                                                              +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:215:52
+   |
+LL |     /// also avoids the need to import `OpenOptions`.
+   |                                                    ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |     /// you can write `File::with_options().read(true).open("foo.txt")`. This
+   |                                                                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// also avoids the need to import `OpenOptions\`.
+   |                                                    +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:220:46
+   |
+LL |     /// `HybridBitSet`. Has no effect if `row` does not exist.
+   |                                              ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |     /// Subtracts `set` from `row`. `set` can be either `BitSet` or
+   |                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// `HybridBitSet`. Has no effect if `row\` does not exist.
+   |                                              +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:246:12
+   |
+LL |     /// RWU`s can get very large, so it uses a more compact representation.
+   |            ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL |     /// `RWU`s can get very large, so it uses a more compact representation.
+   |         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// RWU\`s can get very large, so it uses a more compact representation.
+   |            +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:253:15
+   |
+LL |     /// in `U2`.
+   |               ^
+   |
+help: the opening backtick of a previous inline code may be missing
+   |
+LL |     /// `'?1` is in `U3` would be canonicalized to have `?0` in `U1` and `'?1`
+   |                                                         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// in `U2\`.
+   |               +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:270:42
+   |
+LL |     /// because it contains `[type error]`. Yuck! (See issue #29857 for
+   |                                          ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |     /// as Trait>::Foo == $0`. Here, normalizing `<MyType<$0>` as
+   |                                                              +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// because it contains `[type error]\`. Yuck! (See issue #29857 for
+   |                                          +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:280:53
+   |
+LL |     /// well as the second instance of `A: AutoTrait`) to suppress
+   |                                                     ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL |     /// encountered a problem (later on) with `A:` AutoTrait. So we
+   |                                                  +
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// well as the second instance of `A: AutoTrait\`) to suppress
+   |                                                     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:290:40
+   |
+LL |     /// `'a` with `'b` and not `'static`. But it will have to do for
+   |                                        ^
+   |
+   = help: the opening or closing backtick of an inline code may be missing
+help: if you meant to use a literal backtick, escape it
+   |
+LL |     /// `'a` with `'b` and not `'static\`. But it will have to do for
+   |                                        +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:299:54
+   |
+LL | /// `None`. Otherwise, it will return `Some(Dispatch)`.
+   |                                                      ^
+   |
+help: the opening backtick of a previous inline code may be missing
+   |
+LL | /// The `Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+   |         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// `None`. Otherwise, it will return `Some(Dispatch)\`.
+   |                                                      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:303:13
+   |
+LL | /// or `None` if it isn't.
+   |             ^
+   |
+   = help: the opening or closing backtick of an inline code may be missing
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// or `None\` if it isn't.
+   |             +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:307:14
+   |
+LL | /// `on_event` should be called.
+   |              ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// Called before the filtered [`Layer`]'s [`on_event`], to determine if
+   |                                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// `on_event\` should be called.
+   |              +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:312:29
+   |
+LL | /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+   |                             ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// Therefore, if the `Filter` will change the value returned by this
+   |                              +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// [`rebuild_interest_cache\`][rebuild] is called after the value of the max
+   |                             +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:322:5
+   |
+LL | /     /// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+LL | |
+LL | |
+LL | |
+...  |
+LL | |     /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+LL | |     /// level changes.
+   | |______________________^
+   |
+   = help: the opening backtick of a previous inline code may be missing
+            change: The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+           to this: The `Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+   = help: if you meant to use a literal backtick, escape it
+            change: `None`. Otherwise, it will return `Some(Dispatch)`.
+           to this: `None`. Otherwise, it will return `Some(Dispatch)\`.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:322:5
+   |
+LL | /     /// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+LL | |
+LL | |
+LL | |
+...  |
+LL | |     /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+LL | |     /// level changes.
+   | |______________________^
+   |
+   = help: the opening or closing backtick of an inline code may be missing
+   = help: if you meant to use a literal backtick, escape it
+            change: or `None` if it isn't.
+           to this: or `None\` if it isn't.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:322:5
+   |
+LL | /     /// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+LL | |
+LL | |
+LL | |
+...  |
+LL | |     /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+LL | |     /// level changes.
+   | |______________________^
+   |
+   = help: a previous inline code might be longer than expected
+            change: Called before the filtered [`Layer]'s [`on_event`], to determine if
+           to this: Called before the filtered [`Layer`]'s [`on_event`], to determine if
+   = help: if you meant to use a literal backtick, escape it
+            change: `on_event` should be called.
+           to this: `on_event\` should be called.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:322:5
+   |
+LL | /     /// The Subscriber` may be accessed by calling [`WeakDispatch::upgrade`],
+LL | |
+LL | |
+LL | |
+...  |
+LL | |     /// [`rebuild_interest_cache`][rebuild] is called after the value of the max
+LL | |     /// level changes.
+   | |______________________^
+   |
+   = help: a previous inline code might be longer than expected
+            change: Therefore, if the `Filter will change the value returned by this
+           to this: Therefore, if the `Filter` will change the value returned by this
+   = help: if you meant to use a literal backtick, escape it
+            change: [`rebuild_interest_cache`][rebuild] is called after the value of the max
+           to this: [`rebuild_interest_cache\`][rebuild] is called after the value of the max
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:11:5
+   |
+LL | /// `
+   |     ^
+   |
+   = help: the opening or closing backtick of an inline code may be missing
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \`
+   |     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:18:7
+   |
+LL | /// \`
+   |       ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `\`
+   |     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \\`
+   |       +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:25:6
+   |
+LL | /// [`link1]
+   |      ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// [`link1`]
+   |            +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// [\`link1]
+   |      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:29:11
+   |
+LL | /// [link2`]
+   |           ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// [`link2`]
+   |      +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// [link2\`]
+   |           +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:33:6
+   |
+LL | /// [`link_long](link_long)
+   |      ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// [`link_long`](link_long)
+   |                +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// [\`link_long](link_long)
+   |      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:37:6
+   |
+LL | /// [`broken-link]
+   |      ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// [`broken-link`]
+   |                  +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// [\`broken-link]
+   |      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:44:8
+   |
+LL | /// <x:`>
+   |        ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `<x:`>
+   |     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// <x:\`>
+   |        +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:54:6
+   |
+LL | /// 🦀`🦀
+   |       ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `🦀`🦀
+   |     +
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// 🦀`🦀`
+   |          +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// 🦀\`🦀
+   |       +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:58:5
+   |
+LL | /// `foo(
+   |     ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// `foo(`
+   |          +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \`foo(
+   |     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:64:14
+   |
+LL | /// `foo `bar`
+   |              ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// `foo` `bar`
+   |         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// `foo `bar\`
+   |              +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:70:5
+   |
+LL | /// `foo(
+   |     ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// not paragraph`
+   |                  +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \`foo(
+   |     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:75:83
+   |
+LL | /// Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`.
+   |                                                                                   ^
+   |
+help: the opening backtick of a previous inline code may be missing
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   |                                               +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// Addition is commutative, which means that add(a, b)` is the same as `add(b, a)\`.
+   |                                                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:79:51
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, b)`)!
+   |                                                   ^
+   |
+help: the opening backtick of a previous inline code may be missing
+   |
+LL | /// You could use this function to add 42 to a number `n` (`add(n, 42)`),
+   |                                                            +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, b)\`)!
+   |                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:83:83
+   |
+LL | /// Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`.
+   |                                                                                   ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   |                                                         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// Addition is commutative, which means that `add(a, b) is the same as `add(b, a)\`.
+   |                                                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:87:51
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, n)`)!
+   |                                                   ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// You could use this function to add 42 to a number `n` (`add(n, 42)`),
+   |                                                                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, n)\`)!
+   |                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:91:83
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`.
+   |                                                                                   ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   |                                                                          +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as add(b, a)\`.
+   |                                                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:95:50
+   |
+LL | /// or even to add a number `n` to 42 (add(42, n)`)!
+   |                                                  ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, n)`)!
+   |                                        +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// or even to add a number `n` to 42 (add(42, n)\`)!
+   |                                                  +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:99:74
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a).
+   |                                                                          ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   |                                                                                    +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// Addition is commutative, which means that `add(a, b)` is the same as \`add(b, a).
+   |                                                                          +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:103:51
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, n)`)!
+   |                                                   ^
+   |
+help: a previous inline code might be longer than expected
+   |
+LL | /// You could use this function to add 42 to a number `n` (`add(n, 42)`),
+   |                                                                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// or even to add a number `n` to 42 (`add(42, n)\`)!
+   |                                                   +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:107:1
+   |
+LL | #[doc = "`"]
+   | ^^^^^^^^^^^^
+   |
+   = help: the opening or closing backtick of an inline code may be missing
+   = help: if you meant to use a literal backtick, escape it
+            change: `
+           to this: \`
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:114:1
+   |
+LL | #[doc = concat!("\\", "`")]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: the opening backtick of an inline code may be missing
+            change: \`
+           to this: `\`
+   = help: if you meant to use a literal backtick, escape it
+            change: \`
+           to this: \\`
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:118:1
+   |
+LL | #[doc = "Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`."]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: the opening backtick of a previous inline code may be missing
+            change: Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`.
+           to this: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   = help: if you meant to use a literal backtick, escape it
+            change: Addition is commutative, which means that add(a, b)` is the same as `add(b, a)`.
+           to this: Addition is commutative, which means that add(a, b)` is the same as `add(b, a)\`.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:122:1
+   |
+LL | #[doc = "Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`."]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: a previous inline code might be longer than expected
+            change: Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`.
+           to this: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   = help: if you meant to use a literal backtick, escape it
+            change: Addition is commutative, which means that `add(a, b) is the same as `add(b, a)`.
+           to this: Addition is commutative, which means that `add(a, b) is the same as `add(b, a)\`.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:126:1
+   |
+LL | #[doc = "Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`."]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: the opening backtick of an inline code may be missing
+            change: Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`.
+           to this: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   = help: if you meant to use a literal backtick, escape it
+            change: Addition is commutative, which means that `add(a, b)` is the same as add(b, a)`.
+           to this: Addition is commutative, which means that `add(a, b)` is the same as add(b, a)\`.
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:130:1
+   |
+LL | #[doc = "Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)."]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: the closing backtick of an inline code may be missing
+            change: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a).
+           to this: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a)`.
+   = help: if you meant to use a literal backtick, escape it
+            change: Addition is commutative, which means that `add(a, b)` is the same as `add(b, a).
+           to this: Addition is commutative, which means that `add(a, b)` is the same as \`add(b, a).
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:135:5
+   |
+LL | /// `foo
+   |     ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// `foo`
+   |         +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \`foo
+   |     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:139:7
+   |
+LL | /// # `(heading
+   |       ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// # `(heading`
+   |                +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// # \`(heading
+   |       +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:141:17
+   |
+LL | /// ## heading2)`
+   |                 ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// ## `heading2)`
+   |        +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// ## heading2)\`
+   |                 +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:144:11
+   |
+LL | /// multi `(
+   |           ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// )` heading
+   |      +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// multi \`(
+   |           +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:150:10
+   |
+LL | /// para)`(graph
+   |          ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `para)`(graph
+   |     +
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// para)`(graph`
+   |                 +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// para)\`(graph
+   |          +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:153:10
+   |
+LL | /// para)`(graph2
+   |          ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `para)`(graph2
+   |     +
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// para)`(graph2`
+   |                  +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// para)\`(graph2
+   |          +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:156:12
+   |
+LL | /// 1. foo)`
+   |            ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// 1. `foo)`
+   |        +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// 1. foo)\`
+   |            +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:158:8
+   |
+LL | /// 2. `(bar
+   |        ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// 2. `(bar`
+   |             +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// 2. \`(bar
+   |        +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:160:11
+   |
+LL | /// * baz)`
+   |           ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// * `baz)`
+   |       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// * baz)\`
+   |           +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:162:7
+   |
+LL | /// * `(quux
+   |       ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// * `(quux`
+   |             +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// * \`(quux
+   |       +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:165:5
+   |
+LL | /// `#![this_is_actually_an_image(and(not), an = "attribute")]
+   |     ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// `#`![this_is_actually_an_image(and(not), an = "attribute")]
+   |       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// \`#![this_is_actually_an_image(and(not), an = "attribute")]
+   |     +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:168:62
+   |
+LL | /// #![this_is_actually_an_image(and(not), an = "attribute")]`
+   |                                                              ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// `#![this_is_actually_an_image(and(not), an = "attribute")]`
+   |     +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// #![this_is_actually_an_image(and(not), an = "attribute")]\`
+   |                                                              +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:173:7
+   |
+LL | /// | `table( | )head` |
+   |       ^
+   |
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// | `table(` | )head` |
+   |              +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// | \`table( | )head` |
+   |       +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:173:22
+   |
+LL | /// | `table( | )head` |
+   |                      ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// | `table( | `)head` |
+   |                 +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// | `table( | )head\` |
+   |                      +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:177:12
+   |
+LL | /// | table`( | )`body |
+   |            ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// | `table`( | )`body |
+   |       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// | table\`( | )`body |
+   |            +
+
+error: unescaped backtick
+  --> $DIR/unescaped_backticks.rs:177:18
+   |
+LL | /// | table`( | )`body |
+   |                  ^
+   |
+help: the opening backtick of an inline code may be missing
+   |
+LL | /// | table`( | `)`body |
+   |                 +
+help: the closing backtick of an inline code may be missing
+   |
+LL | /// | table`( | )`body` |
+   |                       +
+help: if you meant to use a literal backtick, escape it
+   |
+LL | /// | table`( | )\`body |
+   |                  +
+
+error: aborting due to 63 previous errors
+