about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock4
-rw-r--r--compiler/rustc_arena/src/lib.rs42
-rw-r--r--src/bootstrap/bootstrap.py35
-rw-r--r--src/doc/rustdoc/src/lints.md38
-rw-r--r--src/librustdoc/html/render/mod.rs18
-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--src/tools/compiletest/src/header.rs11
-rw-r--r--src/tools/compiletest/src/header/cfg.rs6
-rw-r--r--src/tools/compiletest/src/runtest.rs25
-rw-r--r--tests/rustdoc-ui/unescaped_backticks.rs342
-rw-r--r--tests/rustdoc-ui/unescaped_backticks.stderr959
-rw-r--r--tests/rustdoc/deref/deref-const-fn.rs (renamed from tests/rustdoc/deref-const-fn.rs)0
-rw-r--r--tests/rustdoc/deref/deref-multiple-impl-blocks.rs43
-rw-r--r--tests/rustdoc/deref/deref-mut-methods.rs (renamed from tests/rustdoc/deref-mut-methods.rs)0
-rw-r--r--tests/rustdoc/deref/deref-recursive-pathbuf.rs (renamed from tests/rustdoc/deref-recursive-pathbuf.rs)0
-rw-r--r--tests/rustdoc/deref/deref-recursive.rs (renamed from tests/rustdoc/deref-recursive.rs)0
-rw-r--r--tests/rustdoc/deref/deref-slice-core.rs (renamed from tests/rustdoc/deref-slice-core.rs)0
-rw-r--r--tests/rustdoc/deref/deref-to-primitive.rs (renamed from tests/rustdoc/deref-to-primitive.rs)0
-rw-r--r--tests/rustdoc/deref/deref-typedef.rs (renamed from tests/rustdoc/deref-typedef.rs)0
-rw-r--r--tests/rustdoc/deref/escape-deref-methods.rs (renamed from tests/rustdoc/escape-deref-methods.rs)0
-rw-r--r--tests/rustdoc/deref/issue-100679-sidebar-links-deref.rs (renamed from tests/rustdoc/issue-100679-sidebar-links-deref.rs)0
-rw-r--r--tests/rustdoc/deref/recursive-deref-sidebar.rs (renamed from tests/rustdoc/recursive-deref-sidebar.rs)0
-rw-r--r--tests/rustdoc/deref/recursive-deref.rs (renamed from tests/rustdoc/recursive-deref.rs)0
-rw-r--r--tests/ui/feature-gates/test-listing-format-json.rs18
-rw-r--r--tests/ui/feature-gates/test-listing-format-json.run.stderr1
-rw-r--r--tests/ui/test-attrs/tests-listing-format-json.rs1
-rw-r--r--tests/ui/test-attrs/tests-listing-format-json.run.stdout6
30 files changed, 1952 insertions, 38 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 683331d4724..dc12737851e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2370,9 +2370,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.84"
+version = "0.9.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a20eace9dc2d82904039cb76dcf50fb1a0bba071cfd1629720b5d6f1ddba0fa"
+checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
 dependencies = [
  "cc",
  "libc",
diff --git a/compiler/rustc_arena/src/lib.rs b/compiler/rustc_arena/src/lib.rs
index 236bdb99709..6e15f06a76d 100644
--- a/compiler/rustc_arena/src/lib.rs
+++ b/compiler/rustc_arena/src/lib.rs
@@ -20,6 +20,7 @@
 #![feature(rustc_attrs)]
 #![cfg_attr(test, feature(test))]
 #![feature(strict_provenance)]
+#![deny(unsafe_op_in_unsafe_fn)]
 #![deny(rustc::untranslatable_diagnostic)]
 #![deny(rustc::diagnostic_outside_of_impl)]
 #![allow(clippy::mut_from_ref)] // Arena allocators are one of the places where this pattern is fine.
@@ -74,19 +75,27 @@ impl<T> ArenaChunk<T> {
     #[inline]
     unsafe fn new(capacity: usize) -> ArenaChunk<T> {
         ArenaChunk {
-            storage: NonNull::new_unchecked(Box::into_raw(Box::new_uninit_slice(capacity))),
+            storage: NonNull::from(Box::leak(Box::new_uninit_slice(capacity))),
             entries: 0,
         }
     }
 
     /// Destroys this arena chunk.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that `len` elements of this chunk have been initialized.
     #[inline]
     unsafe fn destroy(&mut self, len: usize) {
         // The branch on needs_drop() is an -O1 performance optimization.
-        // Without the branch, dropping TypedArena<u8> takes linear time.
+        // Without the branch, dropping TypedArena<T> takes linear time.
         if mem::needs_drop::<T>() {
-            let slice = self.storage.as_mut();
-            ptr::drop_in_place(MaybeUninit::slice_assume_init_mut(&mut slice[..len]));
+            // SAFETY: The caller must ensure that `len` elements of this chunk have
+            // been initialized.
+            unsafe {
+                let slice = self.storage.as_mut();
+                ptr::drop_in_place(MaybeUninit::slice_assume_init_mut(&mut slice[..len]));
+            }
         }
     }
 
@@ -255,7 +264,9 @@ impl<T> TypedArena<T> {
         self.ensure_capacity(len);
 
         let start_ptr = self.ptr.get();
-        self.ptr.set(start_ptr.add(len));
+        // SAFETY: `self.ensure_capacity` makes sure that there is enough space
+        // for `len` elements.
+        unsafe { self.ptr.set(start_ptr.add(len)) };
         start_ptr
     }
 
@@ -483,6 +494,10 @@ impl DroplessArena {
         }
     }
 
+    /// # Safety
+    ///
+    /// The caller must ensure that `mem` is valid for writes up to
+    /// `size_of::<T>() * len`.
     #[inline]
     unsafe fn write_from_iter<T, I: Iterator<Item = T>>(
         &self,
@@ -494,13 +509,18 @@ impl DroplessArena {
         // Use a manual loop since LLVM manages to optimize it better for
         // slice iterators
         loop {
-            let value = iter.next();
-            if i >= len || value.is_none() {
-                // We only return as many items as the iterator gave us, even
-                // though it was supposed to give us `len`
-                return slice::from_raw_parts_mut(mem, i);
+            // SAFETY: The caller must ensure that `mem` is valid for writes up to
+            // `size_of::<T>() * len`.
+            unsafe {
+                match iter.next() {
+                    Some(value) if i < len => mem.add(i).write(value),
+                    Some(_) | None => {
+                        // We only return as many items as the iterator gave us, even
+                        // though it was supposed to give us `len`
+                        return slice::from_raw_parts_mut(mem, i);
+                    }
+                }
             }
-            ptr::write(mem.add(i), value.unwrap());
             i += 1;
         }
     }
diff --git a/src/bootstrap/bootstrap.py b/src/bootstrap/bootstrap.py
index 9c6c917ac4a..ff261ab9832 100644
--- a/src/bootstrap/bootstrap.py
+++ b/src/bootstrap/bootstrap.py
@@ -19,7 +19,10 @@ try:
 except ImportError:
     lzma = None
 
-if sys.platform == 'win32':
+def platform_is_win32():
+    return sys.platform == 'win32'
+
+if platform_is_win32():
     EXE_SUFFIX = ".exe"
 else:
     EXE_SUFFIX = ""
@@ -78,7 +81,6 @@ def _download(path, url, probably_big, verbose, exception):
     if probably_big or verbose:
         print("downloading {}".format(url))
 
-    platform_is_win32 = sys.platform == 'win32'
     try:
         if probably_big or verbose:
             option = "-#"
@@ -86,7 +88,7 @@ def _download(path, url, probably_big, verbose, exception):
             option = "-s"
         # If curl is not present on Win32, we should not sys.exit
         #   but raise `CalledProcessError` or `OSError` instead
-        require(["curl", "--version"], exception=platform_is_win32)
+        require(["curl", "--version"], exception=platform_is_win32())
         with open(path, "wb") as outfile:
             run(["curl", option,
                 "-L", # Follow redirect.
@@ -99,8 +101,8 @@ def _download(path, url, probably_big, verbose, exception):
             )
     except (subprocess.CalledProcessError, OSError, RuntimeError):
         # see http://serverfault.com/questions/301128/how-to-download
-        if platform_is_win32:
-            run(["PowerShell.exe", "/nologo", "-Command",
+        if platform_is_win32():
+            run_powershell([
                  "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
                  "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
                 verbose=verbose,
@@ -174,6 +176,10 @@ def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
         else:
             sys.exit(err)
 
+def run_powershell(script, *args, **kwargs):
+    """Run a powershell script"""
+    run(["PowerShell.exe", "/nologo", "-Command"] + script, *args, **kwargs)
+
 
 def require(cmd, exit=True, exception=False):
     '''Run a command, returning its output.
@@ -229,7 +235,7 @@ def default_build_triple(verbose):
                 print("pre-installed rustc not detected: {}".format(e))
                 print("falling back to auto-detect")
 
-    required = sys.platform != 'win32'
+    required = not platform_is_win32()
     ostype = require(["uname", "-s"], exit=required)
     cputype = require(['uname', '-m'], exit=required)
 
@@ -434,6 +440,23 @@ class RustBuild(object):
                 (not os.path.exists(self.rustc()) or
                  self.program_out_of_date(self.rustc_stamp(), key)):
             if os.path.exists(bin_root):
+                # HACK: On Windows, we can't delete rust-analyzer-proc-macro-server while it's
+                # running. Kill it.
+                if platform_is_win32():
+                    print("Killing rust-analyzer-proc-macro-srv before deleting stage0 toolchain")
+                    regex =  '{}\\\\(host|{})\\\\stage0\\\\libexec'.format(
+                        os.path.basename(self.build_dir),
+                        self.build
+                    )
+                    script = (
+                        # NOTE: can't use `taskkill` or `Get-Process -Name` because they error if
+                        # the server isn't running.
+                        'Get-Process | ' +
+                        'Where-Object {$_.Name -eq "rust-analyzer-proc-macro-srv"} |' +
+                        'Where-Object {{$_.Path -match "{}"}} |'.format(regex) +
+                        'Stop-Process'
+                    )
+                    run_powershell([script])
                 shutil.rmtree(bin_root)
             tarball_suffix = '.tar.gz' if lzma is None else '.tar.xz'
             filename = "rust-std-{}-{}{}".format(
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/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index a5f08fdac11..d90d0aecb93 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -1155,10 +1155,10 @@ fn render_assoc_items_inner(
     let (non_trait, traits): (Vec<_>, _) = v.iter().partition(|i| i.inner_impl().trait_.is_none());
     if !non_trait.is_empty() {
         let mut tmp_buf = Buffer::html();
-        let (render_mode, id) = match what {
+        let (render_mode, id, class_html) = match what {
             AssocItemRender::All => {
                 write_impl_section_heading(&mut tmp_buf, "Implementations", "implementations");
-                (RenderMode::Normal, "implementations-list".to_owned())
+                (RenderMode::Normal, "implementations-list".to_owned(), "")
             }
             AssocItemRender::DerefFor { trait_, type_, deref_mut_ } => {
                 let id =
@@ -1175,7 +1175,11 @@ fn render_assoc_items_inner(
                     ),
                     &id,
                 );
-                (RenderMode::ForDeref { mut_: deref_mut_ }, cx.derive_id(id))
+                (
+                    RenderMode::ForDeref { mut_: deref_mut_ },
+                    cx.derive_id(id),
+                    r#" class="impl-items""#,
+                )
             }
         };
         let mut impls_buf = Buffer::html();
@@ -1199,7 +1203,7 @@ fn render_assoc_items_inner(
         }
         if !impls_buf.is_empty() {
             write!(w, "{}", tmp_buf.into_inner()).unwrap();
-            write!(w, "<div id=\"{}\">", id).unwrap();
+            write!(w, "<div id=\"{id}\"{class_html}>").unwrap();
             write!(w, "{}", impls_buf.into_inner()).unwrap();
             w.write_str("</div>").unwrap();
         }
@@ -1788,12 +1792,14 @@ fn render_impl(
                 .into_string()
             );
         }
+        if !default_impl_items.is_empty() || !impl_items.is_empty() {
+            w.write_str("<div class=\"impl-items\">");
+            close_tags.insert_str(0, "</div>");
+        }
     }
     if !default_impl_items.is_empty() || !impl_items.is_empty() {
-        w.write_str("<div class=\"impl-items\">");
         w.push_buffer(default_impl_items);
         w.push_buffer(impl_items);
-        close_tags.insert_str(0, "</div>");
     }
     w.write_str(&close_tags);
 }
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/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs
index 01da5981015..8cc935e54d1 100644
--- a/src/tools/compiletest/src/header.rs
+++ b/src/tools/compiletest/src/header.rs
@@ -90,6 +90,9 @@ pub struct TestProps {
     pub unset_rustc_env: Vec<String>,
     // Environment settings to use during execution
     pub exec_env: Vec<(String, String)>,
+    // Environment variables to unset prior to execution.
+    // Variables are unset before applying 'exec_env'
+    pub unset_exec_env: Vec<String>,
     // Build documentation for all specified aux-builds as well
     pub build_aux_docs: bool,
     // Flag to force a crate to be built with the host architecture
@@ -198,6 +201,7 @@ mod directives {
     pub const AUX_CRATE: &'static str = "aux-crate";
     pub const EXEC_ENV: &'static str = "exec-env";
     pub const RUSTC_ENV: &'static str = "rustc-env";
+    pub const UNSET_EXEC_ENV: &'static str = "unset-exec-env";
     pub const UNSET_RUSTC_ENV: &'static str = "unset-rustc-env";
     pub const FORBID_OUTPUT: &'static str = "forbid-output";
     pub const CHECK_TEST_LINE_NUMBERS_MATCH: &'static str = "check-test-line-numbers-match";
@@ -231,6 +235,7 @@ impl TestProps {
             rustc_env: vec![],
             unset_rustc_env: vec![],
             exec_env: vec![],
+            unset_exec_env: vec![],
             build_aux_docs: false,
             force_host: false,
             check_stdout: false,
@@ -384,6 +389,12 @@ impl TestProps {
                 );
                 config.push_name_value_directive(
                     ln,
+                    UNSET_EXEC_ENV,
+                    &mut self.unset_exec_env,
+                    |r| r,
+                );
+                config.push_name_value_directive(
+                    ln,
                     RUSTC_ENV,
                     &mut self.rustc_env,
                     Config::parse_env,
diff --git a/src/tools/compiletest/src/header/cfg.rs b/src/tools/compiletest/src/header/cfg.rs
index a9694d4d52c..86a749b935d 100644
--- a/src/tools/compiletest/src/header/cfg.rs
+++ b/src/tools/compiletest/src/header/cfg.rs
@@ -165,11 +165,15 @@ pub(super) fn parse_cfg_name_directive<'a>(
         message: "when the architecture is part of the Thumb family"
     }
 
+    // Technically the locally built compiler uses the "dev" channel rather than the "nightly"
+    // channel, even though most people don't know or won't care about it. To avoid confusion, we
+    // treat the "dev" channel as the "nightly" channel when processing the directive.
     condition! {
-        name: &config.channel,
+        name: if config.channel == "dev" { "nightly" } else { &config.channel },
         allowed_names: &["stable", "beta", "nightly"],
         message: "when the release channel is {name}",
     }
+
     condition! {
         name: "cross-compile",
         condition: config.target != config.host,
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index e03a73c4e71..0fd9f3ae3f4 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -1614,8 +1614,13 @@ impl<'test> TestCx<'test> {
                 test_client
                     .args(&["run", &support_libs.len().to_string(), &prog])
                     .args(support_libs)
-                    .args(args)
-                    .envs(env.clone());
+                    .args(args);
+
+                for key in &self.props.unset_exec_env {
+                    test_client.env_remove(key);
+                }
+                test_client.envs(env.clone());
+
                 self.compose_and_run(
                     test_client,
                     self.config.run_lib_path.to_str().unwrap(),
@@ -1627,7 +1632,13 @@ impl<'test> TestCx<'test> {
                 let aux_dir = self.aux_output_dir_name();
                 let ProcArgs { prog, args } = self.make_run_args();
                 let mut wr_run = Command::new("wr-run");
-                wr_run.args(&[&prog]).args(args).envs(env.clone());
+                wr_run.args(&[&prog]).args(args);
+
+                for key in &self.props.unset_exec_env {
+                    wr_run.env_remove(key);
+                }
+                wr_run.envs(env.clone());
+
                 self.compose_and_run(
                     wr_run,
                     self.config.run_lib_path.to_str().unwrap(),
@@ -1639,7 +1650,13 @@ impl<'test> TestCx<'test> {
                 let aux_dir = self.aux_output_dir_name();
                 let ProcArgs { prog, args } = self.make_run_args();
                 let mut program = Command::new(&prog);
-                program.args(args).current_dir(&self.output_base_dir()).envs(env.clone());
+                program.args(args).current_dir(&self.output_base_dir());
+
+                for key in &self.props.unset_exec_env {
+                    program.env_remove(key);
+                }
+                program.envs(env.clone());
+
                 self.compose_and_run(
                     program,
                     self.config.run_lib_path.to_str().unwrap(),
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
+
diff --git a/tests/rustdoc/deref-const-fn.rs b/tests/rustdoc/deref/deref-const-fn.rs
index 8ecca6d12d2..8ecca6d12d2 100644
--- a/tests/rustdoc/deref-const-fn.rs
+++ b/tests/rustdoc/deref/deref-const-fn.rs
diff --git a/tests/rustdoc/deref/deref-multiple-impl-blocks.rs b/tests/rustdoc/deref/deref-multiple-impl-blocks.rs
new file mode 100644
index 00000000000..fa3607c5fc1
--- /dev/null
+++ b/tests/rustdoc/deref/deref-multiple-impl-blocks.rs
@@ -0,0 +1,43 @@
+#![crate_name="foo"]
+
+use std::ops::{Deref, DerefMut};
+
+// @has foo/struct.Vec.html
+// @count - '//h2[@id="deref-methods-Slice"]' 1
+// @count - '//div[@id="deref-methods-Slice-1"]' 1
+// @count - '//div[@id="deref-methods-Slice-1"][@class="impl-items"]' 1
+// @count - '//div[@id="deref-methods-Slice-1"]/div[@class="impl-items"]' 0
+pub struct Vec;
+
+pub struct Slice;
+
+impl Deref for Vec {
+    type Target = Slice;
+    fn deref(&self) -> &Slice {
+        &Slice
+    }
+}
+
+impl DerefMut for Vec {
+    fn deref_mut(&mut self) -> &mut Slice {
+        &mut Slice
+    }
+}
+
+impl Slice {
+    pub fn sort_floats(&mut self) {
+        todo!();
+    }
+}
+
+impl Slice {
+    pub fn sort(&mut self) {
+        todo!();
+    }
+}
+
+impl Slice {
+    pub fn len(&self) {
+        todo!();
+    }
+}
diff --git a/tests/rustdoc/deref-mut-methods.rs b/tests/rustdoc/deref/deref-mut-methods.rs
index fdf8434224f..fdf8434224f 100644
--- a/tests/rustdoc/deref-mut-methods.rs
+++ b/tests/rustdoc/deref/deref-mut-methods.rs
diff --git a/tests/rustdoc/deref-recursive-pathbuf.rs b/tests/rustdoc/deref/deref-recursive-pathbuf.rs
index be2b42b5ac6..be2b42b5ac6 100644
--- a/tests/rustdoc/deref-recursive-pathbuf.rs
+++ b/tests/rustdoc/deref/deref-recursive-pathbuf.rs
diff --git a/tests/rustdoc/deref-recursive.rs b/tests/rustdoc/deref/deref-recursive.rs
index 0436f2f86f5..0436f2f86f5 100644
--- a/tests/rustdoc/deref-recursive.rs
+++ b/tests/rustdoc/deref/deref-recursive.rs
diff --git a/tests/rustdoc/deref-slice-core.rs b/tests/rustdoc/deref/deref-slice-core.rs
index cccf273a820..cccf273a820 100644
--- a/tests/rustdoc/deref-slice-core.rs
+++ b/tests/rustdoc/deref/deref-slice-core.rs
diff --git a/tests/rustdoc/deref-to-primitive.rs b/tests/rustdoc/deref/deref-to-primitive.rs
index 527de780d48..527de780d48 100644
--- a/tests/rustdoc/deref-to-primitive.rs
+++ b/tests/rustdoc/deref/deref-to-primitive.rs
diff --git a/tests/rustdoc/deref-typedef.rs b/tests/rustdoc/deref/deref-typedef.rs
index 32424d13eb8..32424d13eb8 100644
--- a/tests/rustdoc/deref-typedef.rs
+++ b/tests/rustdoc/deref/deref-typedef.rs
diff --git a/tests/rustdoc/escape-deref-methods.rs b/tests/rustdoc/deref/escape-deref-methods.rs
index 66919d73eeb..66919d73eeb 100644
--- a/tests/rustdoc/escape-deref-methods.rs
+++ b/tests/rustdoc/deref/escape-deref-methods.rs
diff --git a/tests/rustdoc/issue-100679-sidebar-links-deref.rs b/tests/rustdoc/deref/issue-100679-sidebar-links-deref.rs
index f09d2320609..f09d2320609 100644
--- a/tests/rustdoc/issue-100679-sidebar-links-deref.rs
+++ b/tests/rustdoc/deref/issue-100679-sidebar-links-deref.rs
diff --git a/tests/rustdoc/recursive-deref-sidebar.rs b/tests/rustdoc/deref/recursive-deref-sidebar.rs
index 619f40eff89..619f40eff89 100644
--- a/tests/rustdoc/recursive-deref-sidebar.rs
+++ b/tests/rustdoc/deref/recursive-deref-sidebar.rs
diff --git a/tests/rustdoc/recursive-deref.rs b/tests/rustdoc/deref/recursive-deref.rs
index aa38485c445..aa38485c445 100644
--- a/tests/rustdoc/recursive-deref.rs
+++ b/tests/rustdoc/deref/recursive-deref.rs
diff --git a/tests/ui/feature-gates/test-listing-format-json.rs b/tests/ui/feature-gates/test-listing-format-json.rs
new file mode 100644
index 00000000000..2dd0e10b521
--- /dev/null
+++ b/tests/ui/feature-gates/test-listing-format-json.rs
@@ -0,0 +1,18 @@
+// no-prefer-dynamic
+// compile-flags: --test
+// run-flags: --list --format json -Zunstable-options
+// run-fail
+// check-run-results
+// ignore-nightly
+// unset-exec-env:RUSTC_BOOTSTRAP
+
+#![cfg(test)]
+#[test]
+fn m_test() {}
+
+#[test]
+#[ignore = "not yet implemented"]
+fn z_test() {}
+
+#[test]
+fn a_test() {}
diff --git a/tests/ui/feature-gates/test-listing-format-json.run.stderr b/tests/ui/feature-gates/test-listing-format-json.run.stderr
new file mode 100644
index 00000000000..e81cb81f32c
--- /dev/null
+++ b/tests/ui/feature-gates/test-listing-format-json.run.stderr
@@ -0,0 +1 @@
+error: the option `Z` is only accepted on the nightly compiler
diff --git a/tests/ui/test-attrs/tests-listing-format-json.rs b/tests/ui/test-attrs/tests-listing-format-json.rs
index 18f1521eeeb..5afc2746fe4 100644
--- a/tests/ui/test-attrs/tests-listing-format-json.rs
+++ b/tests/ui/test-attrs/tests-listing-format-json.rs
@@ -3,6 +3,7 @@
 // run-flags: --list --format json -Zunstable-options
 // run-pass
 // check-run-results
+// only-nightly
 // normalize-stdout-test: "fake-test-src-base/test-attrs/" -> "$$DIR/"
 // normalize-stdout-test: "fake-test-src-base\\test-attrs\\" -> "$$DIR/"
 
diff --git a/tests/ui/test-attrs/tests-listing-format-json.run.stdout b/tests/ui/test-attrs/tests-listing-format-json.run.stdout
index b4131e97c34..33cc939b59f 100644
--- a/tests/ui/test-attrs/tests-listing-format-json.run.stdout
+++ b/tests/ui/test-attrs/tests-listing-format-json.run.stdout
@@ -1,5 +1,5 @@
 { "type": "suite", "event": "discovery" }
-{ "type": "test", "event": "discovered", "name": "a_test", "ignore": false, "ignore_message": "", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 20, "start_col": 4, "end_line": 20, "end_col": 10 }
-{ "type": "test", "event": "discovered", "name": "m_test", "ignore": false, "ignore_message": "", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 13, "start_col": 4, "end_line": 13, "end_col": 10 }
-{ "type": "test", "event": "discovered", "name": "z_test", "ignore": true, "ignore_message": "not yet implemented", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 17, "start_col": 4, "end_line": 17, "end_col": 10 }
+{ "type": "test", "event": "discovered", "name": "a_test", "ignore": false, "ignore_message": "", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 21, "start_col": 4, "end_line": 21, "end_col": 10 }
+{ "type": "test", "event": "discovered", "name": "m_test", "ignore": false, "ignore_message": "", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 14, "start_col": 4, "end_line": 14, "end_col": 10 }
+{ "type": "test", "event": "discovered", "name": "z_test", "ignore": true, "ignore_message": "not yet implemented", "source_path": "$DIR/tests-listing-format-json.rs", "start_line": 18, "start_col": 4, "end_line": 18, "end_col": 10 }
 { "type": "suite", "event": "completed", "tests": 3, "benchmarks": 0, "total": 3, "ignored": 1 }