From aff497f17f4264fcaaa36d4db83968bbe2b8ef19 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Mon, 10 Feb 2025 14:32:36 -0700 Subject: [PATCH] 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. --- clippy_lints/src/doc/mod.rs | 110 ++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index c0ee904002c9..fc6d6c4282cd 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, 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, Range)>>( + 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!("{}", doc[start..end].replace('`', "")); + diag.span_suggestion_verbose( + span, + "wrap the entire group in `` 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, Range { - 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!("{}", doc[start..end].replace('`', "")); - diag.span_suggestion_verbose( - span, - "wrap the entire group in `` 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("