Skip to content

Commit

Permalink
doc_nested_refdefs: new lint for suspicious list syntax (rust-lang#13707
Browse files Browse the repository at this point in the history
)

rust-lang/rust#133150

This is more likely to be intended as an intra-doc link than it is to be
intended as a refdef. If a refdef is intended, it does not need to be
nested within a list item.

```markdown
- [`LONG_INTRA_DOC_LINK`]: this
  looks like an intra-doc link,
  but is actually a refdef.
  The first line will seem to
  disappear when rendered as HTML.
```

> - [`LONG_INTRA_DOC_LINK`]: this
>   looks like an intra-doc link,
>   but is actually a refdef.
>   The first line will seem to
>   disappear when rendered as HTML.

changelog: [`doc_nested_refdefs`]: add suspicious lint for link def at
start of list items and block quotes
  • Loading branch information
Jarcho authored Dec 2, 2024
2 parents df46e4c + 8dd45f1 commit 66b15ad
Show file tree
Hide file tree
Showing 9 changed files with 812 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5490,6 +5490,7 @@ Released 2018-09-13
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
[`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
[`double_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_comparisons
[`double_must_use`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_must_use
[`double_neg`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_neg
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::doc::DOC_LAZY_CONTINUATION_INFO,
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
crate::doc::DOC_MARKDOWN_INFO,
crate::doc::DOC_NESTED_REFDEFS_INFO,
crate::doc::EMPTY_DOCS_INFO,
crate::doc::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
crate::doc::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
Expand Down
111 changes: 106 additions & 5 deletions clippy_lints/src/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod too_long_first_doc_paragraph;

use clippy_config::Conf;
use clippy_utils::attrs::is_doc_hidden;
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then};
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
use clippy_utils::ty::is_type_diagnostic_item;
use clippy_utils::visitors::Visitable;
Expand All @@ -18,6 +18,7 @@ use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, It
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};
use rustc_ast::ast::Attribute;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::intravisit::{self, Visitor};
use rustc_hir::{AnonConst, Expr, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
Expand Down Expand Up @@ -564,6 +565,32 @@ declare_clippy_lint! {
"check if files included in documentation are behind `cfg(doc)`"
}

declare_clippy_lint! {
/// ### What it does
/// Warns if a link reference definition appears at the start of a
/// list item or quote.
///
/// ### Why is this bad?
/// This is probably intended as an intra-doc link. If it is really
/// supposed to be a reference definition, it can be written outside
/// of the list item or quote.
///
/// ### Example
/// ```no_run
/// //! - [link]: description
/// ```
/// Use instead:
/// ```no_run
/// //! - [link][]: description (for intra-doc link)
/// //!
/// //! [link]: destination (for link reference definition)
/// ```
#[clippy::version = "1.84.0"]
pub DOC_NESTED_REFDEFS,
suspicious,
"link reference defined in list item or quote"
}

pub struct Documentation {
valid_idents: FxHashSet<String>,
check_private_items: bool,
Expand All @@ -581,6 +608,7 @@ impl Documentation {
impl_lint_pass!(Documentation => [
DOC_LINK_WITH_QUOTES,
DOC_MARKDOWN,
DOC_NESTED_REFDEFS,
MISSING_SAFETY_DOC,
MISSING_ERRORS_DOC,
MISSING_PANICS_DOC,
Expand Down Expand Up @@ -832,6 +860,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
Start(BlockQuote(_)) => {
blockquote_level += 1;
containers.push(Container::Blockquote);
if let Some((next_event, next_range)) = events.peek() {
let next_start = match next_event {
End(TagEnd::BlockQuote) => next_range.end,
_ => next_range.start,
};
if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&
let Some(refdefspan) = fragments.span(cx, refdefrange.clone())
{
span_lint_and_then(
cx,
DOC_NESTED_REFDEFS,
refdefspan,
"link reference defined in quote",
|diag| {
diag.span_suggestion_short(
refdefspan.shrink_to_hi(),
"for an intra-doc link, add `[]` between the label and the colon",
"[]",
Applicability::MaybeIncorrect,
);
diag.help("link definitions are not shown in rendered documentation");
}
);
}
}
},
End(TagEnd::BlockQuote) => {
blockquote_level -= 1;
Expand Down Expand Up @@ -870,11 +923,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
in_heading = true;
}
if let Start(Item) = event {
if let Some((_next_event, next_range)) = events.peek() {
containers.push(Container::List(next_range.start - range.start));
let indent = if let Some((next_event, next_range)) = events.peek() {
let next_start = match next_event {
End(TagEnd::Item) => next_range.end,
_ => next_range.start,
};
if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&
let Some(refdefspan) = fragments.span(cx, refdefrange.clone())
{
span_lint_and_then(
cx,
DOC_NESTED_REFDEFS,
refdefspan,
"link reference defined in list item",
|diag| {
diag.span_suggestion_short(
refdefspan.shrink_to_hi(),
"for an intra-doc link, add `[]` between the label and the colon",
"[]",
Applicability::MaybeIncorrect,
);
diag.help("link definitions are not shown in rendered documentation");
}
);
refdefrange.start - range.start
} else {
next_range.start - range.start
}
} else {
containers.push(Container::List(0));
}
0
};
containers.push(Container::List(indent));
}
ticks_unbalanced = false;
paragraph_range = range;
Expand Down Expand Up @@ -1046,3 +1125,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
self.cx.tcx.hir()
}
}

#[expect(clippy::range_plus_one)] // inclusive ranges aren't the same type
fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {
let offset = range.start;
let mut iterator = doc.as_bytes()[range].iter().copied().enumerate();
let mut start = None;
while let Some((i, byte)) = iterator.next() {
match byte {
b'\\' => {
iterator.next();
},
b'[' => {
start = Some(i + offset);
},
b']' if let Some(start) = start => {
return Some(start..i + offset + 1);
},
_ => {},
}
}
None
}
133 changes: 133 additions & 0 deletions tests/ui/doc/doc_nested_refdef_blockquote.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// https://github.com/rust-lang/rust/issues/133150
#![warn(clippy::doc_nested_refdefs)]
#[rustfmt::skip]
/// > [link][]: def
//~^ ERROR: link reference defined in quote
///
/// > [link][]: def (title)
//~^ ERROR: link reference defined in quote
///
/// > [link][]: def "title"
//~^ ERROR: link reference defined in quote
///
/// > [link]: not def
///
/// > [link][]: notdef
///
/// > [link]\: notdef
pub struct Empty;

#[rustfmt::skip]
/// > [link][]: def
//~^ ERROR: link reference defined in quote
/// > inner text
///
/// > [link][]: def (title)
//~^ ERROR: link reference defined in quote
/// > inner text
///
/// > [link][]: def "title"
//~^ ERROR: link reference defined in quote
/// > inner text
///
/// > [link]: not def
/// > inner text
///
/// > [link][]: notdef
/// > inner text
///
/// > [link]\: notdef
/// > inner text
pub struct NotEmpty;

#[rustfmt::skip]
/// > [link][]: def
//~^ ERROR: link reference defined in quote
/// >
/// > inner text
///
/// > [link][]: def (title)
//~^ ERROR: link reference defined in quote
/// >
/// > inner text
///
/// > [link][]: def "title"
//~^ ERROR: link reference defined in quote
/// >
/// > inner text
///
/// > [link]: not def
/// >
/// > inner text
///
/// > [link][]: notdef
/// >
/// > inner text
///
/// > [link]\: notdef
/// >
/// > inner text
pub struct NotEmptyLoose;

#[rustfmt::skip]
/// > first lines
/// > [link]: def
///
/// > first lines
/// > [link]: def (title)
///
/// > firs lines
/// > [link]: def "title"
///
/// > firs lines
/// > [link]: not def
///
/// > first lines
/// > [link][]: notdef
///
/// > first lines
/// > [link]\: notdef
pub struct NotAtStartTight;

#[rustfmt::skip]
/// > first lines
/// >
/// > [link]: def
///
/// > first lines
/// >
/// > [link]: def (title)
///
/// > firs lines
/// >
/// > [link]: def "title"
///
/// > firs lines
/// >
/// > [link]: not def
///
/// > first lines
/// >
/// > [link][]: notdef
///
/// > first lines
/// >
/// > [link]\: notdef
pub struct NotAtStartLoose;

#[rustfmt::skip]
/// > - [link][]: def
//~^ ERROR: link reference defined in list item
/// >
/// > - [link][]: def (title)
//~^ ERROR: link reference defined in list item
/// >
/// > - [link][]: def "title"
//~^ ERROR: link reference defined in list item
/// >
/// > - [link]: not def
/// >
/// > - [link][]: notdef
/// >
/// > - [link]\: notdef
pub struct ListNested;
Loading

0 comments on commit 66b15ad

Please sign in to comment.