about summary refs log tree commit diff
path: root/src/librustdoc
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-07-04 05:11:57 +0000
committerbors <bors@rust-lang.org>2024-07-04 05:11:57 +0000
commit4a4a81aec43d87abc962cb288af104531086afe8 (patch)
treed3be0ce1b9ff8dada8e736c6921bb9b5c134dffd /src/librustdoc
parentb0d791d3cf13a7b61b3db49591b432d88af2aec8 (diff)
parentd24bfd48b25a3aced787a774c9d679956f164cc6 (diff)
downloadrust-4a4a81aec43d87abc962cb288af104531086afe8.tar.gz
rust-4a4a81aec43d87abc962cb288af104531086afe8.zip
Auto merge of #3731 - rust-lang:rustup-2024-07-04, r=saethlin
Automatic Rustup
Diffstat (limited to 'src/librustdoc')
-rw-r--r--src/librustdoc/Cargo.toml1
-rw-r--r--src/librustdoc/clean/types.rs2
-rw-r--r--src/librustdoc/html/markdown.rs111
-rw-r--r--src/librustdoc/html/render/write_shared.rs3
-rw-r--r--src/librustdoc/lint.rs9
-rw-r--r--src/librustdoc/passes/lint.rs2
-rw-r--r--src/librustdoc/passes/lint/bare_urls.rs4
-rw-r--r--src/librustdoc/passes/lint/html_tags.rs6
-rw-r--r--src/librustdoc/passes/lint/redundant_explicit_links.rs16
-rw-r--r--src/librustdoc/passes/lint/unportable_markdown.rs152
10 files changed, 235 insertions, 71 deletions
diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml
index 31222f213d8..51fb126cb34 100644
--- a/src/librustdoc/Cargo.toml
+++ b/src/librustdoc/Cargo.toml
@@ -13,6 +13,7 @@ base64 = "0.21.7"
 itertools = "0.12"
 indexmap = "2"
 minifier = "0.3.0"
+pulldown-cmark-old = { version = "0.9.6", package = "pulldown-cmark", default-features = false }
 regex = "1"
 rustdoc-json-types = { path = "../rustdoc-json-types" }
 serde_json = "1.0"
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 9050a1c1207..3b1a8830e1c 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -2557,7 +2557,7 @@ pub(crate) struct ProcMacro {
 /// * the `A: Bound` in `Trait<A: Bound>`
 /// * the `RetTy` in `Trait(ArgTy, ArgTy) -> RetTy`
 /// * the `C = { Ct }` in `Trait<C = { Ct }>` (feature `associated_const_equality`)
-/// * the `f(): Bound` in `Trait<f(): Bound>` (feature `return_type_notation`)
+/// * the `f(..): Bound` in `Trait<f(..): Bound>` (feature `return_type_notation`)
 #[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub(crate) struct AssocItemConstraint {
     pub(crate) assoc: PathSegment,
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index bae929c64ea..a7f0df5afa9 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -54,7 +54,8 @@ use crate::html::render::small_url_encode;
 use crate::html::toc::TocBuilder;
 
 use pulldown_cmark::{
-    html, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, OffsetIter, Options, Parser, Tag,
+    html, BrokenLink, BrokenLinkCallback, CodeBlockKind, CowStr, Event, LinkType, OffsetIter,
+    Options, Parser, Tag, TagEnd,
 };
 
 #[cfg(test)]
@@ -230,7 +231,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
         let mut original_text = String::new();
         for event in &mut self.inner {
             match event {
-                Event::End(Tag::CodeBlock(..)) => break,
+                Event::End(TagEnd::CodeBlock) => break,
                 Event::Text(ref s) => {
                     original_text.push_str(s);
                 }
@@ -359,16 +360,17 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
         match &mut event {
             // This is a shortcut link that was resolved by the broken_link_callback: `[fn@f]`
             // Remove any disambiguator.
-            Some(Event::Start(Tag::Link(
+            Some(Event::Start(Tag::Link {
                 // [fn@f] or [fn@f][]
-                LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
-                dest,
+                link_type: LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
+                dest_url,
                 title,
-            ))) => {
-                debug!("saw start of shortcut link to {dest} with title {title}");
+                ..
+            })) => {
+                debug!("saw start of shortcut link to {dest_url} with title {title}");
                 // If this is a shortcut link, it was resolved by the broken_link_callback.
                 // So the URL will already be updated properly.
-                let link = self.links.iter().find(|&link| *link.href == **dest);
+                let link = self.links.iter().find(|&link| *link.href == **dest_url);
                 // Since this is an external iterator, we can't replace the inner text just yet.
                 // Store that we saw a link so we know to replace it later.
                 if let Some(link) = link {
@@ -381,16 +383,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                 }
             }
             // Now that we're done with the shortcut link, don't replace any more text.
-            Some(Event::End(Tag::Link(
-                LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
-                dest,
-                _,
-            ))) => {
-                debug!("saw end of shortcut link to {dest}");
-                if self.links.iter().any(|link| *link.href == **dest) {
-                    assert!(self.shortcut_link.is_some(), "saw closing link without opening tag");
-                    self.shortcut_link = None;
-                }
+            Some(Event::End(TagEnd::Link)) if self.shortcut_link.is_some() => {
+                debug!("saw end of shortcut link");
+                self.shortcut_link = None;
             }
             // Handle backticks in inline code blocks, but only if we're in the middle of a shortcut link.
             // [`fn@f`]
@@ -433,9 +428,11 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             }
             // If this is a link, but not a shortcut link,
             // replace the URL, since the broken_link_callback was not called.
-            Some(Event::Start(Tag::Link(_, dest, title))) => {
-                if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) {
-                    *dest = CowStr::Borrowed(link.href.as_ref());
+            Some(Event::Start(Tag::Link { dest_url, title, .. })) => {
+                if let Some(link) =
+                    self.links.iter().find(|&link| *link.original_text == **dest_url)
+                {
+                    *dest_url = CowStr::Borrowed(link.href.as_ref());
                     if title.is_empty() && !link.tooltip.is_empty() {
                         *title = CowStr::Borrowed(link.tooltip.as_ref());
                     }
@@ -477,9 +474,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for TableWrapper<'a, I> {
                 self.stored_events.push_back(Event::Start(Tag::Table(t)));
                 Event::Html(CowStr::Borrowed("<div>"))
             }
-            Event::End(Tag::Table(t)) => {
+            Event::End(TagEnd::Table) => {
                 self.stored_events.push_back(Event::Html(CowStr::Borrowed("</div>")));
-                Event::End(Tag::Table(t))
+                Event::End(TagEnd::Table)
             }
             e => e,
         })
@@ -519,11 +516,11 @@ impl<'a, 'b, 'ids, I: Iterator<Item = SpannedEvent<'a>>> Iterator
         }
 
         let event = self.inner.next();
-        if let Some((Event::Start(Tag::Heading(level, _, _)), _)) = event {
+        if let Some((Event::Start(Tag::Heading { level, .. }), _)) = event {
             let mut id = String::new();
             for event in &mut self.inner {
                 match &event.0 {
-                    Event::End(Tag::Heading(..)) => break,
+                    Event::End(TagEnd::Heading(_)) => break,
                     Event::Text(text) | Event::Code(text) => {
                         id.extend(text.chars().filter_map(slugify));
                         self.buf.push_back(event);
@@ -566,27 +563,27 @@ impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> {
     }
 }
 
-fn check_if_allowed_tag(t: &Tag<'_>) -> bool {
+fn check_if_allowed_tag(t: &TagEnd) -> bool {
     matches!(
         t,
-        Tag::Paragraph
-            | Tag::Emphasis
-            | Tag::Strong
-            | Tag::Strikethrough
-            | Tag::Link(..)
-            | Tag::BlockQuote
+        TagEnd::Paragraph
+            | TagEnd::Emphasis
+            | TagEnd::Strong
+            | TagEnd::Strikethrough
+            | TagEnd::Link
+            | TagEnd::BlockQuote
     )
 }
 
-fn is_forbidden_tag(t: &Tag<'_>) -> bool {
+fn is_forbidden_tag(t: &TagEnd) -> bool {
     matches!(
         t,
-        Tag::CodeBlock(_)
-            | Tag::Table(_)
-            | Tag::TableHead
-            | Tag::TableRow
-            | Tag::TableCell
-            | Tag::FootnoteDefinition(_)
+        TagEnd::CodeBlock
+            | TagEnd::Table
+            | TagEnd::TableHead
+            | TagEnd::TableRow
+            | TagEnd::TableCell
+            | TagEnd::FootnoteDefinition
     )
 }
 
@@ -604,12 +601,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
             let mut is_start = true;
             let is_allowed_tag = match event {
                 Event::Start(ref c) => {
-                    if is_forbidden_tag(c) {
+                    if is_forbidden_tag(&c.to_end()) {
                         self.skipped_tags += 1;
                         return None;
                     }
                     self.depth += 1;
-                    check_if_allowed_tag(c)
+                    check_if_allowed_tag(&c.to_end())
                 }
                 Event::End(ref c) => {
                     if is_forbidden_tag(c) {
@@ -633,7 +630,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
                 if is_start {
                     Some(Event::Start(Tag::Paragraph))
                 } else {
-                    Some(Event::End(Tag::Paragraph))
+                    Some(Event::End(TagEnd::Paragraph))
                 }
             } else {
                 Some(event)
@@ -679,7 +676,7 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
                 Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
                     let mut content = Vec::new();
                     for (event, _) in &mut self.inner {
-                        if let Event::End(Tag::FootnoteDefinition(..)) = event {
+                        if let Event::End(TagEnd::FootnoteDefinition) = event {
                             break;
                         }
                         content.push(event);
@@ -696,7 +693,7 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
                         for (mut content, id) in v {
                             write!(ret, "<li id=\"fn{id}\">").unwrap();
                             let mut is_paragraph = false;
-                            if let Some(&Event::End(Tag::Paragraph)) = content.last() {
+                            if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
                                 content.pop();
                                 is_paragraph = true;
                             }
@@ -806,7 +803,7 @@ pub(crate) fn find_codes<T: doctest::DoctestVisitor>(
                 tests.visit_test(text, block_info, line);
                 prev_offset = offset.start;
             }
-            Event::Start(Tag::Heading(level, _, _)) => {
+            Event::Start(Tag::Heading { level, .. }) => {
                 register_header = Some(level as u32);
             }
             Event::Text(ref s) if register_header.is_some() => {
@@ -1432,7 +1429,7 @@ impl MarkdownItemInfo<'_> {
 
         // Treat inline HTML as plain text.
         let p = p.map(|event| match event.0 {
-            Event::Html(text) => (Event::Text(text), event.1),
+            Event::Html(text) | Event::InlineHtml(text) => (Event::Text(text), event.1),
             _ => event,
         });
 
@@ -1442,7 +1439,7 @@ impl MarkdownItemInfo<'_> {
         let p = Footnotes::new(p);
         let p = TableWrapper::new(p.map(|(ev, _)| ev));
         let p = p.filter(|event| {
-            !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
+            !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
         });
         html::push_html(&mut s, p);
 
@@ -1472,7 +1469,7 @@ impl MarkdownSummaryLine<'_> {
         let mut s = String::new();
 
         let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
-            !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
+            !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
         });
 
         html::push_html(&mut s, without_paragraphs);
@@ -1544,8 +1541,8 @@ fn markdown_summary_with_limit(
                 _ => {}
             },
             Event::End(tag) => match tag {
-                Tag::Emphasis | Tag::Strong => buf.close_tag(),
-                Tag::Paragraph | Tag::Heading(..) => return ControlFlow::Break(()),
+                TagEnd::Emphasis | TagEnd::Strong => buf.close_tag(),
+                TagEnd::Paragraph | TagEnd::Heading(_) => return ControlFlow::Break(()),
                 _ => {}
             },
             Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
@@ -1605,8 +1602,8 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
             }
             Event::HardBreak | Event::SoftBreak => s.push(' '),
             Event::Start(Tag::CodeBlock(..)) => break,
-            Event::End(Tag::Paragraph) => break,
-            Event::End(Tag::Heading(..)) => break,
+            Event::End(TagEnd::Paragraph) => break,
+            Event::End(TagEnd::Heading(..)) => break,
             _ => (),
         }
     }
@@ -1765,7 +1762,7 @@ pub(crate) fn markdown_links<'md, R>(
 
     while let Some((event, span)) = event_iter.next() {
         match event {
-            Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => {
+            Event::Start(Tag::Link { link_type, dest_url, .. }) if may_be_doc_link(link_type) => {
                 let range = match link_type {
                     // Link is pulled from the link itself.
                     LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
@@ -1775,7 +1772,7 @@ pub(crate) fn markdown_links<'md, R>(
                     LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
                     // Link is pulled from elsewhere in the document.
                     LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
-                        span_for_link(&dest, span)
+                        span_for_link(&dest_url, span)
                     }
                     LinkType::Autolink | LinkType::Email => unreachable!(),
                 };
@@ -1795,7 +1792,7 @@ pub(crate) fn markdown_links<'md, R>(
 
                 if let Some(link) = preprocess_link(MarkdownLink {
                     kind: link_type,
-                    link: dest.into_string(),
+                    link: dest_url.into_string(),
                     display_text,
                     range,
                 }) {
@@ -1810,8 +1807,8 @@ pub(crate) fn markdown_links<'md, R>(
 }
 
 /// Collects additional data of link.
-fn collect_link_data<'input, 'callback>(
-    event_iter: &mut OffsetIter<'input, 'callback>,
+fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
+    event_iter: &mut OffsetIter<'input, F>,
 ) -> Option<String> {
     let mut display_text: Option<String> = None;
     let mut append_text = |text: CowStr<'_>| {
diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs
index c806bf1cc66..8fd56eae37f 100644
--- a/src/librustdoc/html/render/write_shared.rs
+++ b/src/librustdoc/html/render/write_shared.rs
@@ -507,8 +507,7 @@ else if (window.initSearch) window.initSearch(searchIndex);
                 // Be aware of `tests/rustdoc/type-alias/deeply-nested-112515.rs` which might regress.
                 let Some(impl_did) = impl_item_id.as_def_id() else { continue };
                 let for_ty = self.cx.tcx().type_of(impl_did).skip_binder();
-                let reject_cx =
-                    DeepRejectCtxt { treat_obligation_params: TreatParams::AsCandidateKey };
+                let reject_cx = DeepRejectCtxt::new(self.cx.tcx(), TreatParams::AsCandidateKey);
                 if !reject_cx.types_may_unify(aliased_ty, for_ty) {
                     continue;
                 }
diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs
index dd2bb47e592..8eaca70eaff 100644
--- a/src/librustdoc/lint.rs
+++ b/src/librustdoc/lint.rs
@@ -196,6 +196,14 @@ declare_rustdoc_lint! {
     "detects redundant explicit links in doc comments"
 }
 
+declare_rustdoc_lint! {
+    /// This compatibility lint checks for Markdown syntax that works in the old engine but not
+    /// the new one.
+    UNPORTABLE_MARKDOWN,
+    Warn,
+    "detects markdown that is interpreted differently in different parser"
+}
+
 pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
     vec![
         BROKEN_INTRA_DOC_LINKS,
@@ -209,6 +217,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
         MISSING_CRATE_LEVEL_DOCS,
         UNESCAPED_BACKTICKS,
         REDUNDANT_EXPLICIT_LINKS,
+        UNPORTABLE_MARKDOWN,
     ]
 });
 
diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs
index c6d5b7bd346..bc804a340bf 100644
--- a/src/librustdoc/passes/lint.rs
+++ b/src/librustdoc/passes/lint.rs
@@ -6,6 +6,7 @@ mod check_code_block_syntax;
 mod html_tags;
 mod redundant_explicit_links;
 mod unescaped_backticks;
+mod unportable_markdown;
 
 use super::Pass;
 use crate::clean::*;
@@ -31,6 +32,7 @@ impl<'a, 'tcx> DocVisitor for Linter<'a, 'tcx> {
         html_tags::visit_item(self.cx, item);
         unescaped_backticks::visit_item(self.cx, item);
         redundant_explicit_links::visit_item(self.cx, item);
+        unportable_markdown::visit_item(self.cx, item);
 
         self.visit_item_recur(item)
     }
diff --git a/src/librustdoc/passes/lint/bare_urls.rs b/src/librustdoc/passes/lint/bare_urls.rs
index 8f68f6ff476..4b2d3092837 100644
--- a/src/librustdoc/passes/lint/bare_urls.rs
+++ b/src/librustdoc/passes/lint/bare_urls.rs
@@ -42,11 +42,11 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item) {
             match event {
                 Event::Text(s) => find_raw_urls(cx, &s, range, &report_diag),
                 // We don't want to check the text inside code blocks or links.
-                Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link(..))) => {
+                Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link { .. })) => {
                     while let Some((event, _)) = p.next() {
                         match event {
                             Event::End(end)
-                                if mem::discriminant(&end) == mem::discriminant(&tag) =>
+                                if mem::discriminant(&end) == mem::discriminant(&tag.to_end()) =>
                             {
                                 break;
                             }
diff --git a/src/librustdoc/passes/lint/html_tags.rs b/src/librustdoc/passes/lint/html_tags.rs
index a0064a90112..87dfa5d5389 100644
--- a/src/librustdoc/passes/lint/html_tags.rs
+++ b/src/librustdoc/passes/lint/html_tags.rs
@@ -4,7 +4,7 @@ use crate::clean::*;
 use crate::core::DocContext;
 use crate::html::markdown::main_body_opts;
 
-use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag};
+use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag, TagEnd};
 use rustc_resolve::rustdoc::source_span_for_markdown_range;
 
 use std::iter::Peekable;
@@ -140,10 +140,10 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
         for (event, range) in p {
             match event {
                 Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
-                Event::Html(text) if !in_code_block => {
+                Event::Html(text) | Event::InlineHtml(text) if !in_code_block => {
                     extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag)
                 }
-                Event::End(Tag::CodeBlock(_)) => in_code_block = false,
+                Event::End(TagEnd::CodeBlock) => in_code_block = false,
                 _ => {}
             }
         }
diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs
index 7ab974046b9..b36b41c9f2d 100644
--- a/src/librustdoc/passes/lint/redundant_explicit_links.rs
+++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs
@@ -1,6 +1,8 @@
 use std::ops::Range;
 
-use pulldown_cmark::{BrokenLink, CowStr, Event, LinkType, OffsetIter, Parser, Tag};
+use pulldown_cmark::{
+    BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, OffsetIter, Parser, Tag,
+};
 use rustc_ast::NodeId;
 use rustc_errors::SuggestionStyle;
 use rustc_hir::def::{DefKind, DocLinkResMap, Namespace, Res};
@@ -95,7 +97,7 @@ fn check_redundant_explicit_link<'md>(
 
     while let Some((event, link_range)) = offset_iter.next() {
         match event {
-            Event::Start(Tag::Link(link_type, dest, _)) => {
+            Event::Start(Tag::Link { link_type, dest_url, .. }) => {
                 let link_data = collect_link_data(&mut offset_iter);
 
                 if let Some(resolvable_link) = link_data.resolvable_link.as_ref() {
@@ -108,7 +110,7 @@ fn check_redundant_explicit_link<'md>(
                     }
                 }
 
-                let explicit_link = dest.to_string();
+                let explicit_link = dest_url.to_string();
                 let display_link = link_data.resolvable_link.clone()?;
 
                 if explicit_link.ends_with(&display_link) || display_link.ends_with(&explicit_link)
@@ -122,7 +124,7 @@ fn check_redundant_explicit_link<'md>(
                                 doc,
                                 resolutions,
                                 link_range,
-                                dest.to_string(),
+                                dest_url.to_string(),
                                 link_data,
                                 if link_type == LinkType::Inline {
                                     (b'(', b')')
@@ -139,7 +141,7 @@ fn check_redundant_explicit_link<'md>(
                                 doc,
                                 resolutions,
                                 link_range,
-                                &dest,
+                                &dest_url,
                                 link_data,
                             );
                         }
@@ -259,7 +261,9 @@ fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId
 }
 
 /// Collects all necessary data of link.
-fn collect_link_data(offset_iter: &mut OffsetIter<'_, '_>) -> LinkData {
+fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
+    offset_iter: &mut OffsetIter<'input, F>,
+) -> LinkData {
     let mut resolvable_link = None;
     let mut resolvable_link_range = None;
     let mut display_link = String::new();
diff --git a/src/librustdoc/passes/lint/unportable_markdown.rs b/src/librustdoc/passes/lint/unportable_markdown.rs
new file mode 100644
index 00000000000..5f185377634
--- /dev/null
+++ b/src/librustdoc/passes/lint/unportable_markdown.rs
@@ -0,0 +1,152 @@
+//! Detects specific markdown syntax that's different between pulldown-cmark
+//! 0.9 and 0.11.
+//!
+//! This is a mitigation for old parser bugs that affected some
+//! real crates' docs. The old parser claimed to comply with CommonMark,
+//! but it did not. These warnings will eventually be removed,
+//! though some of them may become Clippy lints.
+//!
+//! <https://github.com/rust-lang/rust/pull/121659#issuecomment-1992752820>
+//!
+//! <https://rustc-dev-guide.rust-lang.org/bug-fix-procedure.html#add-the-lint-to-the-list-of-removed-lists>
+
+use crate::clean::Item;
+use crate::core::DocContext;
+use pulldown_cmark as cmarkn;
+use pulldown_cmark_old as cmarko;
+use rustc_lint_defs::Applicability;
+use rustc_resolve::rustdoc::source_span_for_markdown_range;
+use std::collections::{BTreeMap, BTreeSet};
+
+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.doc_value();
+    if dox.is_empty() {
+        return;
+    }
+
+    // P1: unintended strikethrough was fixed by requiring single-tildes to flank
+    // the same way underscores do, so nothing is done here
+
+    // P2: block quotes without following space parsed wrong
+    //
+    // This is the set of starting points for block quotes with no space after
+    // the `>`. It is populated by the new parser, and if the old parser fails to
+    // clear it out, it'll produce a warning.
+    let mut spaceless_block_quotes = BTreeSet::new();
+
+    // P3: missing footnote references
+    //
+    // This is populated by listening for FootnoteReference from
+    // the new parser and old parser.
+    let mut missing_footnote_references = BTreeMap::new();
+    let mut found_footnote_references = BTreeSet::new();
+
+    // populate problem cases from new parser
+    {
+        pub fn main_body_opts_new() -> cmarkn::Options {
+            cmarkn::Options::ENABLE_TABLES
+                | cmarkn::Options::ENABLE_FOOTNOTES
+                | cmarkn::Options::ENABLE_STRIKETHROUGH
+                | cmarkn::Options::ENABLE_TASKLISTS
+                | cmarkn::Options::ENABLE_SMART_PUNCTUATION
+        }
+        let mut parser_new = cmarkn::Parser::new_ext(&dox, main_body_opts_new()).into_offset_iter();
+        while let Some((event, span)) = parser_new.next() {
+            if let cmarkn::Event::Start(cmarkn::Tag::BlockQuote(_)) = event {
+                if !dox[span.clone()].starts_with("> ") {
+                    spaceless_block_quotes.insert(span.start);
+                }
+            }
+            if let cmarkn::Event::FootnoteReference(_) = event {
+                found_footnote_references.insert(span.start + 1);
+            }
+        }
+    }
+
+    // remove cases where they don't actually differ
+    {
+        pub fn main_body_opts_old() -> cmarko::Options {
+            cmarko::Options::ENABLE_TABLES
+                | cmarko::Options::ENABLE_FOOTNOTES
+                | cmarko::Options::ENABLE_STRIKETHROUGH
+                | cmarko::Options::ENABLE_TASKLISTS
+                | cmarko::Options::ENABLE_SMART_PUNCTUATION
+        }
+        let mut parser_old = cmarko::Parser::new_ext(&dox, main_body_opts_old()).into_offset_iter();
+        while let Some((event, span)) = parser_old.next() {
+            if let cmarko::Event::Start(cmarko::Tag::BlockQuote) = event {
+                if !dox[span.clone()].starts_with("> ") {
+                    spaceless_block_quotes.remove(&span.start);
+                }
+            }
+            if let cmarko::Event::FootnoteReference(_) = event {
+                if !found_footnote_references.contains(&(span.start + 1)) {
+                    missing_footnote_references.insert(span.start + 1, span);
+                }
+            }
+        }
+    }
+
+    for start in spaceless_block_quotes {
+        let (span, precise) =
+            source_span_for_markdown_range(tcx, &dox, &(start..start + 1), &item.attrs.doc_strings)
+                .map(|span| (span, true))
+                .unwrap_or_else(|| (item.attr_span(tcx), false));
+
+        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, span, |lint| {
+            lint.primary_message("unportable markdown");
+            lint.help(format!("confusing block quote with no space after the `>` marker"));
+            if precise {
+                lint.span_suggestion(
+                    span.shrink_to_hi(),
+                    "if the quote is intended, add a space",
+                    " ",
+                    Applicability::MaybeIncorrect,
+                );
+                lint.span_suggestion(
+                    span.shrink_to_lo(),
+                    "if it should not be a quote, escape it",
+                    "\\",
+                    Applicability::MaybeIncorrect,
+                );
+            }
+        });
+    }
+    for (_caret, span) in missing_footnote_references {
+        let (ref_span, precise) =
+            source_span_for_markdown_range(tcx, &dox, &span, &item.attrs.doc_strings)
+                .map(|span| (span, true))
+                .unwrap_or_else(|| (item.attr_span(tcx), false));
+
+        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, ref_span, |lint| {
+            lint.primary_message("unportable markdown");
+            if precise {
+                lint.span_suggestion(
+                    ref_span.shrink_to_lo(),
+                    "if it should not be a footnote, escape it",
+                    "\\",
+                    Applicability::MaybeIncorrect,
+                );
+            }
+            if dox.as_bytes().get(span.end) == Some(&b'[') {
+                lint.help("confusing footnote reference and link");
+                if precise {
+                    lint.span_suggestion(
+                        ref_span.shrink_to_hi(),
+                        "if the footnote is intended, add a space",
+                        " ",
+                        Applicability::MaybeIncorrect,
+                    );
+                } else {
+                    lint.help("there should be a space between the link and the footnote");
+                }
+            }
+        });
+    }
+}