about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Howell <michael@notriddle.com>2025-02-10 14:32:36 -0700
committerMichael Howell <michael@notriddle.com>2025-02-10 14:43:03 -0700
commitaff497f17f4264fcaaa36d4db83968bbe2b8ef19 (patch)
tree953c438f99bf1026b3a28aeccec6ef91661ed848
parent045e36d6a7b797e2eef8ee92bce11a499c319a5b (diff)
downloadrust-aff497f17f4264fcaaa36d4db83968bbe2b8ef19.tar.gz
rust-aff497f17f4264fcaaa36d4db83968bbe2b8ef19.zip
Use a separate loop to drive the check for code clusters
By using a separate loop, I can just skip nodes that I don't want
to process twice, instead of having to hand-build a state machine
with an enum.
-rw-r--r--clippy_lints/src/doc/mod.rs110
1 files changed, 73 insertions, 37 deletions
diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs
index c0ee904002c..fc6d6c4282c 100644
--- a/clippy_lints/src/doc/mod.rs
+++ b/clippy_lints/src/doc/mod.rs
@@ -844,6 +844,21 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
 
     let mut cb = fake_broken_link_callback;
 
+    check_for_code_clusters(
+        cx,
+        pulldown_cmark::Parser::new_with_broken_link_callback(
+            &doc,
+            main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,
+            Some(&mut cb),
+        )
+        .into_offset_iter(),
+        &doc,
+        Fragments {
+            doc: &doc,
+            fragments: &fragments,
+        },
+    );
+
     // disable smart punctuation to pick up ['link'] more easily
     let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;
     let parser = pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut cb));
@@ -867,12 +882,64 @@ enum Container {
     List(usize),
 }
 
-#[derive(Clone, Copy, Eq, PartialEq)]
-enum CodeCluster {
-    // true means already in a link, so only needs to be followed by code
-    // false means we've hit code, and need to find a link
-    First(usize, bool),
-    Nth(usize, usize),
+/// Scan the documentation for code links that are back-to-back with code spans.
+///
+/// This is done separately from the rest of the docs, because that makes it easier to produce
+/// the correct messages.
+fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
+    cx: &LateContext<'_>,
+    events: Events,
+    doc: &str,
+    fragments: Fragments<'_>,
+) {
+    let mut events = events.peekable();
+    let mut code_starts_at = None;
+    let mut code_ends_at = None;
+    let mut code_includes_link = false;
+    while let Some((event, range)) = events.next() {
+        match event {
+            Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {
+                if code_starts_at.is_some() {
+                    code_ends_at = Some(range.end);
+                } else {
+                    code_starts_at = Some(range.start);
+                }
+                code_includes_link = true;
+                // skip the nested "code", because we're already handling it here
+                let _ = events.next();
+            },
+            Code(_) => {
+                if code_starts_at.is_some() {
+                    code_ends_at = Some(range.end);
+                } else {
+                    code_starts_at = Some(range.start);
+                }
+            },
+            End(TagEnd::Link) => {},
+            _ => {
+                if let Some(start) = code_starts_at
+                    && let Some(end) = code_ends_at
+                    && code_includes_link
+                {
+                    if let Some(span) = fragments.span(cx, start..end) {
+                        span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
+                            let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
+                            diag.span_suggestion_verbose(
+                                span,
+                                "wrap the entire group in `<code>` tags",
+                                sugg,
+                                Applicability::MaybeIncorrect,
+                            );
+                            diag.help("separate code snippets will be shown with a gap");
+                        });
+                    }
+                }
+                code_includes_link = false;
+                code_starts_at = None;
+                code_ends_at = None;
+            },
+        }
+    }
 }
 
 /// Checks parsed documentation.
@@ -906,40 +973,9 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
 
     let mut containers = Vec::new();
 
-    let mut code_cluster = None;
-
     let mut events = events.peekable();
 
     while let Some((event, range)) = events.next() {
-        code_cluster = match (code_cluster, &event) {
-            (None, Code(_)) if in_link.is_some() && doc.as_bytes().get(range.start.wrapping_sub(1)) == Some(&b'[') => {
-                Some(CodeCluster::First(range.start - 1, true))
-            },
-            (None, Code(_)) => Some(CodeCluster::First(range.start, false)),
-            (Some(CodeCluster::First(pos, _)), Start(Link { .. })) | (Some(CodeCluster::First(pos, true)), Code(_)) => {
-                Some(CodeCluster::Nth(pos, range.end))
-            },
-            (Some(CodeCluster::Nth(start, end)), Code(_) | Start(Link { .. })) => {
-                Some(CodeCluster::Nth(start, range.end.max(end)))
-            },
-            (code_cluster @ Some(_), Code(_) | End(TagEnd::Link)) => code_cluster,
-            (Some(CodeCluster::First(_, _)) | None, _) => None,
-            (Some(CodeCluster::Nth(start, end)), _) => {
-                if let Some(span) = fragments.span(cx, start..end) {
-                    span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
-                        let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
-                        diag.span_suggestion_verbose(
-                            span,
-                            "wrap the entire group in `<code>` tags",
-                            sugg,
-                            Applicability::MaybeIncorrect,
-                        );
-                        diag.help("separate code snippets will be shown with a gap");
-                    });
-                }
-                None
-            },
-        };
         match event {
             Html(tag) | InlineHtml(tag) => {
                 if tag.starts_with("<code") {