Skip to content

Commit

Permalink
doc_link_code: add check for links with code spans that render weird (#…
Browse files Browse the repository at this point in the history
…14121)

This is the lint described at
rust-lang/rust#136308 (comment)
that recommends using HTML to nest links inside code.

changelog: [`doc_link_code`]: warn when a link with code and a code span
are back-to-back
  • Loading branch information
Centri3 authored Feb 14, 2025
2 parents b83762c + aff497f commit 50ecb6e
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5530,6 +5530,7 @@ Released 2018-09-13
[`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression
[`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_include_without_cfg
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
[`doc_link_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_code
[`doc_link_with_quotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_with_quotes
[`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
[`doc_nested_refdefs`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_nested_refdefs
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::disallowed_types::DISALLOWED_TYPES_INFO,
crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO,
crate::doc::DOC_LAZY_CONTINUATION_INFO,
crate::doc::DOC_LINK_CODE_INFO,
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
crate::doc::DOC_MARKDOWN_INFO,
crate::doc::DOC_NESTED_REFDEFS_INFO,
Expand Down
98 changes: 98 additions & 0 deletions clippy_lints/src/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ declare_clippy_lint! {
"presence of `_`, `::` or camel-case outside backticks in documentation"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for links with code directly adjacent to code text:
/// `` [`MyItem`]`<`[`u32`]`>` ``.
///
/// ### Why is this bad?
/// It can be written more simply using HTML-style `<code>` tags.
///
/// ### Example
/// ```no_run
/// //! [`first`](x)`second`
/// ```
/// Use instead:
/// ```no_run
/// //! <code>[first](x)second</code>
/// ```
#[clippy::version = "1.86.0"]
pub DOC_LINK_CODE,
nursery,
"link with code back-to-back with other code"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for the doc comments of publicly visible
Expand Down Expand Up @@ -637,6 +659,7 @@ impl Documentation {
}

impl_lint_pass!(Documentation => [
DOC_LINK_CODE,
DOC_LINK_WITH_QUOTES,
DOC_MARKDOWN,
DOC_NESTED_REFDEFS,
Expand Down Expand Up @@ -820,6 +843,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));
Expand All @@ -843,6 +881,66 @@ enum Container {
List(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.
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
/// so lints here will generally access that information.
Expand Down
52 changes: 52 additions & 0 deletions tests/ui/doc/link_adjacent.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![warn(clippy::doc_link_code)]

//! Test case for code links that are adjacent to code text.
//!
//! This is not an example: `first``second`
//!
//! Neither is this: [`first`](x)
//!
//! Neither is this: [`first`](x) `second`
//!
//! Neither is this: [first](x)`second`
//!
//! This is: <code>[first](x)second</code>
//~^ ERROR: adjacent
//!
//! So is this <code>first[second](x)</code>
//~^ ERROR: adjacent
//!
//! So is this <code>[first](x)[second](x)</code>
//~^ ERROR: adjacent
//!
//! So is this <code>[first](x)[second](x)[third](x)</code>
//~^ ERROR: adjacent
//!
//! So is this <code>[first](x)second[third](x)</code>
//~^ ERROR: adjacent

/// Test case for code links that are adjacent to code text.
///
/// This is not an example: `first``second` arst
///
/// Neither is this: [`first`](x) arst
///
/// Neither is this: [`first`](x) `second` arst
///
/// Neither is this: [first](x)`second` arst
///
/// This is: <code>[first](x)second</code> arst
//~^ ERROR: adjacent
///
/// So is this <code>first[second](x)</code> arst
//~^ ERROR: adjacent
///
/// So is this <code>[first](x)[second](x)</code> arst
//~^ ERROR: adjacent
///
/// So is this <code>[first](x)[second](x)[third](x)</code> arst
//~^ ERROR: adjacent
///
/// So is this <code>[first](x)second[third](x)</code> arst
//~^ ERROR: adjacent
pub struct WithTrailing;
52 changes: 52 additions & 0 deletions tests/ui/doc/link_adjacent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![warn(clippy::doc_link_code)]

//! Test case for code links that are adjacent to code text.
//!
//! This is not an example: `first``second`
//!
//! Neither is this: [`first`](x)
//!
//! Neither is this: [`first`](x) `second`
//!
//! Neither is this: [first](x)`second`
//!
//! This is: [`first`](x)`second`
//~^ ERROR: adjacent
//!
//! So is this `first`[`second`](x)
//~^ ERROR: adjacent
//!
//! So is this [`first`](x)[`second`](x)
//~^ ERROR: adjacent
//!
//! So is this [`first`](x)[`second`](x)[`third`](x)
//~^ ERROR: adjacent
//!
//! So is this [`first`](x)`second`[`third`](x)
//~^ ERROR: adjacent

/// Test case for code links that are adjacent to code text.
///
/// This is not an example: `first``second` arst
///
/// Neither is this: [`first`](x) arst
///
/// Neither is this: [`first`](x) `second` arst
///
/// Neither is this: [first](x)`second` arst
///
/// This is: [`first`](x)`second` arst
//~^ ERROR: adjacent
///
/// So is this `first`[`second`](x) arst
//~^ ERROR: adjacent
///
/// So is this [`first`](x)[`second`](x) arst
//~^ ERROR: adjacent
///
/// So is this [`first`](x)[`second`](x)[`third`](x) arst
//~^ ERROR: adjacent
///
/// So is this [`first`](x)`second`[`third`](x) arst
//~^ ERROR: adjacent
pub struct WithTrailing;
124 changes: 124 additions & 0 deletions tests/ui/doc/link_adjacent.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:13:14
|
LL | //! This is: [`first`](x)`second`
| ^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
= note: `-D clippy::doc-link-code` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::doc_link_code)]`
help: wrap the entire group in `<code>` tags
|
LL | //! This is: <code>[first](x)second</code>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:16:16
|
LL | //! So is this `first`[`second`](x)
| ^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | //! So is this <code>first[second](x)</code>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:19:16
|
LL | //! So is this [`first`](x)[`second`](x)
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | //! So is this <code>[first](x)[second](x)</code>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:22:16
|
LL | //! So is this [`first`](x)[`second`](x)[`third`](x)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | //! So is this <code>[first](x)[second](x)[third](x)</code>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:25:16
|
LL | //! So is this [`first`](x)`second`[`third`](x)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | //! So is this <code>[first](x)second[third](x)</code>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:38:14
|
LL | /// This is: [`first`](x)`second` arst
| ^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | /// This is: <code>[first](x)second</code> arst
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:41:16
|
LL | /// So is this `first`[`second`](x) arst
| ^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | /// So is this <code>first[second](x)</code> arst
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:44:16
|
LL | /// So is this [`first`](x)[`second`](x) arst
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | /// So is this <code>[first](x)[second](x)</code> arst
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:47:16
|
LL | /// So is this [`first`](x)[`second`](x)[`third`](x) arst
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | /// So is this <code>[first](x)[second](x)[third](x)</code> arst
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: code link adjacent to code text
--> tests/ui/doc/link_adjacent.rs:50:16
|
LL | /// So is this [`first`](x)`second`[`third`](x) arst
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: separate code snippets will be shown with a gap
help: wrap the entire group in `<code>` tags
|
LL | /// So is this <code>[first](x)second[third](x)</code> arst
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: aborting due to 10 previous errors

0 comments on commit 50ecb6e

Please sign in to comment.