1
1
use clippy_utils:: attrs:: is_doc_hidden;
2
2
use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then} ;
3
3
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;
5
5
use clippy_utils:: ty:: { implements_trait, is_type_diagnostic_item} ;
6
6
use clippy_utils:: { is_entrypoint_fn, method_chain_args, return_ty} ;
7
7
use if_chain:: if_chain;
8
- use itertools:: Itertools ;
9
8
use pulldown_cmark:: Event :: {
10
9
Code , End , FootnoteReference , HardBreak , Html , Rule , SoftBreak , Start , TaskListMarker , Text ,
11
10
} ;
12
11
use pulldown_cmark:: Tag :: { CodeBlock , Heading , Item , Link , Paragraph } ;
13
12
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 } ;
16
14
use rustc_data_structures:: fx:: FxHashSet ;
17
15
use rustc_data_structures:: sync:: Lrc ;
18
16
use rustc_errors:: emitter:: EmitterWriter ;
@@ -26,6 +24,9 @@ use rustc_middle::lint::in_external_macro;
26
24
use rustc_middle:: ty;
27
25
use rustc_parse:: maybe_new_parser_from_source_str;
28
26
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
+ } ;
29
30
use rustc_session:: parse:: ParseSess ;
30
31
use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
31
32
use rustc_span:: edition:: Edition ;
@@ -450,53 +451,16 @@ fn lint_for_missing_headers(
450
451
}
451
452
}
452
453
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
+ }
470
459
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 )
497
463
}
498
-
499
- ( no_stars, sizes)
500
464
}
501
465
502
466
#[ derive( Copy , Clone , Default ) ]
@@ -515,51 +479,32 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
515
479
Some ( ( "fake" . into ( ) , "fake" . into ( ) ) )
516
480
}
517
481
482
+ let ( fragments, _) = attrs_to_doc_fragments ( attrs. iter ( ) . map ( |attr| ( attr, None ) ) , true ) ;
518
483
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) ;
538
486
}
487
+ doc. pop ( ) ;
539
488
540
489
if doc. is_empty ( ) {
541
490
return Some ( DocHeaders :: default ( ) ) ;
542
491
}
543
492
544
493
let mut cb = fake_broken_link_callback;
545
494
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
+ ) )
563
508
}
564
509
565
510
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
568
513
cx : & LateContext < ' _ > ,
569
514
valid_idents : & FxHashSet < String > ,
570
515
events : Events ,
571
- spans : & [ ( usize , Span ) ] ,
516
+ fragments : Fragments < ' _ > ,
572
517
) -> DocHeaders {
573
518
// true if a safety header was found
574
519
let mut headers = DocHeaders :: default ( ) ;
@@ -579,8 +524,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
579
524
let mut no_test = false ;
580
525
let mut edition = None ;
581
526
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 ;
584
529
for ( event, range) in events {
585
530
match event {
586
531
Start ( CodeBlock ( ref kind) ) => {
@@ -613,25 +558,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
613
558
in_heading = true ;
614
559
}
615
560
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;
618
562
} ,
619
563
End ( Heading ( _, _, _) | Paragraph | Item ) => {
620
564
if let End ( Heading ( _, _, _) ) = event {
621
565
in_heading = false ;
622
566
}
623
- if ticks_unbalanced {
567
+ if ticks_unbalanced
568
+ && let Some ( span) = fragments. span ( cx, paragraph_range. clone ( ) )
569
+ {
624
570
span_lint_and_help (
625
571
cx,
626
572
DOC_MARKDOWN ,
627
- paragraph_span ,
573
+ span ,
628
574
"backticks are unbalanced" ,
629
575
None ,
630
576
"a backtick may be missing a pair" ,
631
577
) ;
632
578
} 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
+ }
635
583
}
636
584
}
637
585
text_to_check = Vec :: new ( ) ;
@@ -640,8 +588,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
640
588
Html ( _html) => ( ) , // HTML is weird, just ignore it
641
589
SoftBreak | HardBreak | TaskListMarker ( _) | Code ( _) | Rule => ( ) ,
642
590
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 ;
645
592
ticks_unbalanced |= text. contains ( '`' ) && !in_code;
646
593
if Some ( & text) == in_link. as_ref ( ) || ticks_unbalanced {
647
594
// 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
658
605
if in_code {
659
606
if is_rust && !no_test {
660
607
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 ) ;
662
609
}
663
610
} 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
+ }
667
614
if let Some ( link) = in_link. as_ref ( )
668
615
&& let Ok ( url) = Url :: parse ( link)
669
616
&& ( url. scheme ( ) == "https" || url. scheme ( ) == "http" ) {
670
617
// Don't check the text associated with external URLs
671
618
continue ;
672
619
}
673
- text_to_check. push ( ( text, span ) ) ;
620
+ text_to_check. push ( ( text, range ) ) ;
674
621
}
675
622
} ,
676
623
}
677
624
}
678
625
headers
679
626
}
680
627
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
+ {
693
633
span_lint (
694
634
cx,
695
635
DOC_LINK_WITH_QUOTES ,
696
- span. with_lo ( lo ) . with_hi ( lo + BytePos :: from_usize ( text_len ) ) ,
636
+ span,
697
637
"possible intra-doc link using quotes instead of backticks" ,
698
638
) ;
699
639
}
700
640
}
701
641
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 < ' _ > ) {
711
643
fn has_needless_main ( code : String , edition : Edition ) -> bool {
712
644
rustc_driver:: catch_fatal_errors ( || {
713
645
rustc_span:: create_session_globals_then ( edition, || {
@@ -774,12 +706,13 @@ fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) {
774
706
. unwrap_or_default ( )
775
707
}
776
708
709
+ let trailing_whitespace = text. len ( ) - text. trim_end ( ) . len ( ) ;
710
+
777
711
// Because of the global session, we need to create a new session in a different thread with
778
712
// the edition we need.
779
713
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)
783
716
{
784
717
span_lint ( cx, NEEDLESS_DOCTEST_MAIN , span, "needless `fn main` in doctest" ) ;
785
718
}
0 commit comments