11use clippy_utils:: attrs:: is_doc_hidden;
22use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then} ;
33use 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;
55use clippy_utils:: ty:: { implements_trait, is_type_diagnostic_item} ;
66use clippy_utils:: { is_entrypoint_fn, method_chain_args, return_ty} ;
77use if_chain:: if_chain;
8- use itertools:: Itertools ;
98use pulldown_cmark:: Event :: {
109 Code , End , FootnoteReference , HardBreak , Html , Rule , SoftBreak , Start , TaskListMarker , Text ,
1110} ;
1211use pulldown_cmark:: Tag :: { CodeBlock , Heading , Item , Link , Paragraph } ;
1312use 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 } ;
1614use rustc_data_structures:: fx:: FxHashSet ;
1715use rustc_data_structures:: sync:: Lrc ;
1816use rustc_errors:: emitter:: EmitterWriter ;
@@ -26,6 +24,9 @@ use rustc_middle::lint::in_external_macro;
2624use rustc_middle:: ty;
2725use rustc_parse:: maybe_new_parser_from_source_str;
2826use 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+ } ;
2930use rustc_session:: parse:: ParseSess ;
3031use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
3132use 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
565510const 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 }
0 commit comments