Skip to content

Commit b643f20

Browse files
committed
Auto merge of #115689 - Alexendoo:clippy-doc-comments, r=notriddle,Manishearth,flip1995
Reuse rustdoc's doc comment handling in Clippy Moves `source_span_for_markdown_range` and `span_of_attrs` (renamed to `span_of_fragments`) to `rustc_resolve::rustdoc` so it can be used in Clippy Fixes #10277 Fixes #5593 Fixes #10263 Fixes #2581
2 parents 54f3ddb + e88a556 commit b643f20

13 files changed

+160
-157
lines changed

clippy_lints/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ clippy_utils = { path = "../clippy_utils" }
1515
declare_clippy_lint = { path = "../declare_clippy_lint" }
1616
if_chain = "1.0"
1717
itertools = "0.10.1"
18-
pulldown-cmark = { version = "0.9", default-features = false }
1918
quine-mc_cluskey = "0.2"
2019
regex-syntax = "0.7"
2120
serde = { version = "1.0", features = ["derive"] }

clippy_lints/src/doc.rs

Lines changed: 59 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
use clippy_utils::attrs::is_doc_hidden;
22
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then};
33
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
4-
use clippy_utils::source::{first_line_of_span, snippet_with_applicability};
4+
use clippy_utils::source::snippet_with_applicability;
55
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
66
use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty};
77
use if_chain::if_chain;
8-
use itertools::Itertools;
98
use pulldown_cmark::Event::{
109
Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text,
1110
};
1211
use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph};
1312
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options};
14-
use rustc_ast::ast::{Async, AttrKind, Attribute, Fn, FnRetTy, ItemKind};
15-
use rustc_ast::token::CommentKind;
13+
use rustc_ast::ast::{Async, Attribute, Fn, FnRetTy, ItemKind};
1614
use rustc_data_structures::fx::FxHashSet;
1715
use rustc_data_structures::sync::Lrc;
1816
use rustc_errors::emitter::EmitterWriter;
@@ -26,6 +24,9 @@ use rustc_middle::lint::in_external_macro;
2624
use rustc_middle::ty;
2725
use rustc_parse::maybe_new_parser_from_source_str;
2826
use rustc_parse::parser::ForceCollect;
27+
use rustc_resolve::rustdoc::{
28+
add_doc_fragment, attrs_to_doc_fragments, main_body_opts, source_span_for_markdown_range, DocFragment,
29+
};
2930
use rustc_session::parse::ParseSess;
3031
use rustc_session::{declare_tool_lint, impl_lint_pass};
3132
use rustc_span::edition::Edition;
@@ -450,53 +451,16 @@ fn lint_for_missing_headers(
450451
}
451452
}
452453

453-
/// Cleanup documentation decoration.
454-
///
455-
/// We can't use `rustc_ast::attr::AttributeMethods::with_desugared_doc` or
456-
/// `rustc_ast::parse::lexer::comments::strip_doc_comment_decoration` because we
457-
/// need to keep track of
458-
/// the spans but this function is inspired from the later.
459-
#[expect(clippy::cast_possible_truncation)]
460-
#[must_use]
461-
pub fn strip_doc_comment_decoration(doc: &str, comment_kind: CommentKind, span: Span) -> (String, Vec<(usize, Span)>) {
462-
// one-line comments lose their prefix
463-
if comment_kind == CommentKind::Line {
464-
let mut doc = doc.to_owned();
465-
doc.push('\n');
466-
let len = doc.len();
467-
// +3 skips the opening delimiter
468-
return (doc, vec![(len, span.with_lo(span.lo() + BytePos(3)))]);
469-
}
454+
#[derive(Copy, Clone)]
455+
struct Fragments<'a> {
456+
doc: &'a str,
457+
fragments: &'a [DocFragment],
458+
}
470459

471-
let mut sizes = vec![];
472-
let mut contains_initial_stars = false;
473-
for line in doc.lines() {
474-
let offset = line.as_ptr() as usize - doc.as_ptr() as usize;
475-
debug_assert_eq!(offset as u32 as usize, offset);
476-
contains_initial_stars |= line.trim_start().starts_with('*');
477-
// +1 adds the newline, +3 skips the opening delimiter
478-
sizes.push((line.len() + 1, span.with_lo(span.lo() + BytePos(3 + offset as u32))));
479-
}
480-
if !contains_initial_stars {
481-
return (doc.to_string(), sizes);
482-
}
483-
// remove the initial '*'s if any
484-
let mut no_stars = String::with_capacity(doc.len());
485-
for line in doc.lines() {
486-
let mut chars = line.chars();
487-
for c in &mut chars {
488-
if c.is_whitespace() {
489-
no_stars.push(c);
490-
} else {
491-
no_stars.push(if c == '*' { ' ' } else { c });
492-
break;
493-
}
494-
}
495-
no_stars.push_str(chars.as_str());
496-
no_stars.push('\n');
460+
impl Fragments<'_> {
461+
fn span(self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> {
462+
source_span_for_markdown_range(cx.tcx, &self.doc, &range, &self.fragments)
497463
}
498-
499-
(no_stars, sizes)
500464
}
501465

502466
#[derive(Copy, Clone, Default)]
@@ -515,51 +479,32 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
515479
Some(("fake".into(), "fake".into()))
516480
}
517481

482+
let (fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
518483
let mut doc = String::new();
519-
let mut spans = vec![];
520-
521-
for attr in attrs {
522-
if let AttrKind::DocComment(comment_kind, comment) = attr.kind {
523-
let (comment, current_spans) = strip_doc_comment_decoration(comment.as_str(), comment_kind, attr.span);
524-
spans.extend_from_slice(&current_spans);
525-
doc.push_str(&comment);
526-
} else if attr.has_name(sym::doc) {
527-
// ignore mix of sugared and non-sugared doc
528-
// don't trigger the safety or errors check
529-
return None;
530-
}
531-
}
532-
533-
let mut current = 0;
534-
for &mut (ref mut offset, _) in &mut spans {
535-
let offset_copy = *offset;
536-
*offset = current;
537-
current += offset_copy;
484+
for fragment in &fragments {
485+
add_doc_fragment(&mut doc, fragment);
538486
}
487+
doc.pop();
539488

540489
if doc.is_empty() {
541490
return Some(DocHeaders::default());
542491
}
543492

544493
let mut cb = fake_broken_link_callback;
545494

546-
let parser =
547-
pulldown_cmark::Parser::new_with_broken_link_callback(&doc, Options::empty(), Some(&mut cb)).into_offset_iter();
548-
// Iterate over all `Events` and combine consecutive events into one
549-
let events = parser.coalesce(|previous, current| {
550-
let previous_range = previous.1;
551-
let current_range = current.1;
552-
553-
match (previous.0, current.0) {
554-
(Text(previous), Text(current)) => {
555-
let mut previous = previous.to_string();
556-
previous.push_str(&current);
557-
Ok((Text(previous.into()), previous_range))
558-
},
559-
(previous, current) => Err(((previous, previous_range), (current, current_range))),
560-
}
561-
});
562-
Some(check_doc(cx, valid_idents, events, &spans))
495+
// disable smart punctuation to pick up ['link'] more easily
496+
let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;
497+
let parser = pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut cb));
498+
499+
Some(check_doc(
500+
cx,
501+
valid_idents,
502+
parser.into_offset_iter(),
503+
Fragments {
504+
fragments: &fragments,
505+
doc: &doc,
506+
},
507+
))
563508
}
564509

565510
const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
@@ -568,7 +513,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
568513
cx: &LateContext<'_>,
569514
valid_idents: &FxHashSet<String>,
570515
events: Events,
571-
spans: &[(usize, Span)],
516+
fragments: Fragments<'_>,
572517
) -> DocHeaders {
573518
// true if a safety header was found
574519
let mut headers = DocHeaders::default();
@@ -579,8 +524,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
579524
let mut no_test = false;
580525
let mut edition = None;
581526
let mut ticks_unbalanced = false;
582-
let mut text_to_check: Vec<(CowStr<'_>, Span)> = Vec::new();
583-
let mut paragraph_span = spans.get(0).expect("function isn't called if doc comment is empty").1;
527+
let mut text_to_check: Vec<(CowStr<'_>, Range<usize>)> = Vec::new();
528+
let mut paragraph_range = 0..0;
584529
for (event, range) in events {
585530
match event {
586531
Start(CodeBlock(ref kind)) => {
@@ -613,25 +558,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
613558
in_heading = true;
614559
}
615560
ticks_unbalanced = false;
616-
let (_, span) = get_current_span(spans, range.start);
617-
paragraph_span = first_line_of_span(cx, span);
561+
paragraph_range = range;
618562
},
619563
End(Heading(_, _, _) | Paragraph | Item) => {
620564
if let End(Heading(_, _, _)) = event {
621565
in_heading = false;
622566
}
623-
if ticks_unbalanced {
567+
if ticks_unbalanced
568+
&& let Some(span) = fragments.span(cx, paragraph_range.clone())
569+
{
624570
span_lint_and_help(
625571
cx,
626572
DOC_MARKDOWN,
627-
paragraph_span,
573+
span,
628574
"backticks are unbalanced",
629575
None,
630576
"a backtick may be missing a pair",
631577
);
632578
} else {
633-
for (text, span) in text_to_check {
634-
check_text(cx, valid_idents, &text, span);
579+
for (text, range) in text_to_check {
580+
if let Some(span) = fragments.span(cx, range) {
581+
check_text(cx, valid_idents, &text, span);
582+
}
635583
}
636584
}
637585
text_to_check = Vec::new();
@@ -640,8 +588,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
640588
Html(_html) => (), // HTML is weird, just ignore it
641589
SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (),
642590
FootnoteReference(text) | Text(text) => {
643-
let (begin, span) = get_current_span(spans, range.start);
644-
paragraph_span = paragraph_span.with_hi(span.hi());
591+
paragraph_range.end = range.end;
645592
ticks_unbalanced |= text.contains('`') && !in_code;
646593
if Some(&text) == in_link.as_ref() || ticks_unbalanced {
647594
// Probably a link of the form `<http://example.com>`
@@ -658,56 +605,41 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
658605
if in_code {
659606
if is_rust && !no_test {
660607
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
661-
check_code(cx, &text, edition, span);
608+
check_code(cx, &text, edition, range.clone(), fragments);
662609
}
663610
} else {
664-
check_link_quotes(cx, in_link.is_some(), trimmed_text, span, &range, begin, text.len());
665-
// Adjust for the beginning of the current `Event`
666-
let span = span.with_lo(span.lo() + BytePos::from_usize(range.start - begin));
611+
if in_link.is_some() {
612+
check_link_quotes(cx, trimmed_text, range.clone(), fragments);
613+
}
667614
if let Some(link) = in_link.as_ref()
668615
&& let Ok(url) = Url::parse(link)
669616
&& (url.scheme() == "https" || url.scheme() == "http") {
670617
// Don't check the text associated with external URLs
671618
continue;
672619
}
673-
text_to_check.push((text, span));
620+
text_to_check.push((text, range));
674621
}
675622
},
676623
}
677624
}
678625
headers
679626
}
680627

681-
fn check_link_quotes(
682-
cx: &LateContext<'_>,
683-
in_link: bool,
684-
trimmed_text: &str,
685-
span: Span,
686-
range: &Range<usize>,
687-
begin: usize,
688-
text_len: usize,
689-
) {
690-
if in_link && trimmed_text.starts_with('\'') && trimmed_text.ends_with('\'') {
691-
// fix the span to only point at the text within the link
692-
let lo = span.lo() + BytePos::from_usize(range.start - begin);
628+
fn check_link_quotes(cx: &LateContext<'_>, trimmed_text: &str, range: Range<usize>, fragments: Fragments<'_>) {
629+
if trimmed_text.starts_with('\'')
630+
&& trimmed_text.ends_with('\'')
631+
&& let Some(span) = fragments.span(cx, range)
632+
{
693633
span_lint(
694634
cx,
695635
DOC_LINK_WITH_QUOTES,
696-
span.with_lo(lo).with_hi(lo + BytePos::from_usize(text_len)),
636+
span,
697637
"possible intra-doc link using quotes instead of backticks",
698638
);
699639
}
700640
}
701641

702-
fn get_current_span(spans: &[(usize, Span)], idx: usize) -> (usize, Span) {
703-
let index = match spans.binary_search_by(|c| c.0.cmp(&idx)) {
704-
Ok(o) => o,
705-
Err(e) => e - 1,
706-
};
707-
spans[index]
708-
}
709-
710-
fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) {
642+
fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range<usize>, fragments: Fragments<'_>) {
711643
fn has_needless_main(code: String, edition: Edition) -> bool {
712644
rustc_driver::catch_fatal_errors(|| {
713645
rustc_span::create_session_globals_then(edition, || {
@@ -774,12 +706,13 @@ fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) {
774706
.unwrap_or_default()
775707
}
776708

709+
let trailing_whitespace = text.len() - text.trim_end().len();
710+
777711
// Because of the global session, we need to create a new session in a different thread with
778712
// the edition we need.
779713
let text = text.to_owned();
780-
if thread::spawn(move || has_needless_main(text, edition))
781-
.join()
782-
.expect("thread::spawn failed")
714+
if thread::spawn(move || has_needless_main(text, edition)).join().expect("thread::spawn failed")
715+
&& let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace)
783716
{
784717
span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
785718
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
// FIXME: switch to something more ergonomic here, once available.
2323
// (Currently there is no way to opt into sysroot crates without `extern crate`.)
24+
extern crate pulldown_cmark;
2425
extern crate rustc_arena;
2526
extern crate rustc_ast;
2627
extern crate rustc_ast_pretty;
@@ -37,6 +38,7 @@ extern crate rustc_lexer;
3738
extern crate rustc_lint;
3839
extern crate rustc_middle;
3940
extern crate rustc_parse;
41+
extern crate rustc_resolve;
4042
extern crate rustc_session;
4143
extern crate rustc_span;
4244
extern crate rustc_target;

tests/compile-test.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ fn base_config(test_dir: &str) -> (Config, Args) {
130130
};
131131

132132
let mut config = Config {
133-
mode: Mode::Yolo { rustfix: RustfixMode::Everything },
133+
mode: Mode::Yolo {
134+
rustfix: RustfixMode::Everything,
135+
},
134136
stderr_filters: vec![(Match::PathBackslash, b"/")],
135137
stdout_filters: vec![],
136138
output_conflict_handling: if bless {

tests/ui/doc/doc-fixable.fixed

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ fn pulldown_cmark_crash() {}
198198
/// [plain text][path::to::item]
199199
fn intra_doc_link() {}
200200

201+
/// Ignore escaped\_underscores
202+
///
203+
/// \\[
204+
/// \\prod\_{x\\in X} p\_x
205+
/// \\]
206+
fn issue_2581() {}
207+
208+
/// Foo \[bar\] \[baz\] \[qux\]. `DocMarkdownLint`
209+
fn lint_after_escaped_chars() {}
210+
201211
// issue #7033 - generic_const_exprs ICE
202212
struct S<T, const N: usize>
203213
where [(); N.checked_next_power_of_two().unwrap()]: {

tests/ui/doc/doc-fixable.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ fn pulldown_cmark_crash() {}
198198
/// [plain text][path::to::item]
199199
fn intra_doc_link() {}
200200

201+
/// Ignore escaped\_underscores
202+
///
203+
/// \\[
204+
/// \\prod\_{x\\in X} p\_x
205+
/// \\]
206+
fn issue_2581() {}
207+
208+
/// Foo \[bar\] \[baz\] \[qux\]. DocMarkdownLint
209+
fn lint_after_escaped_chars() {}
210+
201211
// issue #7033 - generic_const_exprs ICE
202212
struct S<T, const N: usize>
203213
where [(); N.checked_next_power_of_two().unwrap()]: {

tests/ui/doc/doc-fixable.stderr

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,5 +308,16 @@ help: try
308308
LL | /// An iterator over `mycrate::Collection`'s values.
309309
| ~~~~~~~~~~~~~~~~~~~~~
310310

311-
error: aborting due to 28 previous errors
311+
error: item in documentation is missing backticks
312+
--> $DIR/doc-fixable.rs:208:34
313+
|
314+
LL | /// Foo \[bar\] \[baz\] \[qux\]. DocMarkdownLint
315+
| ^^^^^^^^^^^^^^^
316+
|
317+
help: try
318+
|
319+
LL | /// Foo \[bar\] \[baz\] \[qux\]. `DocMarkdownLint`
320+
| ~~~~~~~~~~~~~~~~~
321+
322+
error: aborting due to 29 previous errors
312323

0 commit comments

Comments
 (0)