@@ -24,6 +24,7 @@ use rustc_span::edition::Edition;
2424use std:: ops:: Range ;
2525use url:: Url ;
2626
27+ mod broken_link;
2728mod doc_comment_double_space_linebreaks;
2829mod doc_suspicious_footnotes;
2930mod include_in_doc_without_cfg;
@@ -292,6 +293,34 @@ declare_clippy_lint! {
292293 "possible typo for an intra-doc link"
293294}
294295
296+ declare_clippy_lint ! {
297+ /// ### What it does
298+ /// Checks the doc comments have unbroken links, mostly caused
299+ /// by bad formatted links such as broken across multiple lines.
300+ ///
301+ /// ### Why is this bad?
302+ /// Because documentation generated by rustdoc will be broken
303+ /// since expected links won't be links and just text.
304+ ///
305+ /// ### Examples
306+ /// This link is broken:
307+ /// ```no_run
308+ /// /// [example of a bad link](https://
309+ /// /// github.com/rust-lang/rust-clippy/)
310+ /// pub fn do_something() {}
311+ /// ```
312+ ///
313+ /// It shouldn't be broken across multiple lines to work:
314+ /// ```no_run
315+ /// /// [example of a good link](https://github.com/rust-lang/rust-clippy/)
316+ /// pub fn do_something() {}
317+ /// ```
318+ #[ clippy:: version = "1.84.0" ]
319+ pub DOC_BROKEN_LINK ,
320+ pedantic,
321+ "broken document link"
322+ }
323+
295324declare_clippy_lint ! {
296325 /// ### What it does
297326 /// Checks for the doc comments of publicly visible
@@ -656,6 +685,7 @@ impl Documentation {
656685impl_lint_pass ! ( Documentation => [
657686 DOC_LINK_CODE ,
658687 DOC_LINK_WITH_QUOTES ,
688+ DOC_BROKEN_LINK ,
659689 DOC_MARKDOWN ,
660690 DOC_NESTED_REFDEFS ,
661691 MISSING_SAFETY_DOC ,
@@ -786,9 +816,9 @@ struct DocHeaders {
786816/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or
787817/// "Panics" sections.
788818fn check_attrs ( cx : & LateContext < ' _ > , valid_idents : & FxHashSet < String > , attrs : & [ Attribute ] ) -> Option < DocHeaders > {
789- /// We don't want the parser to choke on intra doc links. Since we don't
790- /// actually care about rendering them, just pretend that all broken links
791- /// point to a fake address.
819+ // We don't want the parser to choke on intra doc links. Since we don't
820+ // actually care about rendering them, just pretend that all broken links
821+ // point to a fake address.
792822 #[ expect( clippy:: unnecessary_wraps) ] // we're following a type signature
793823 fn fake_broken_link_callback < ' a > ( _: BrokenLink < ' _ > ) -> Option < ( CowStr < ' a > , CowStr < ' a > ) > {
794824 Some ( ( "fake" . into ( ) , "fake" . into ( ) ) )
@@ -828,14 +858,12 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
828858 return Some ( DocHeaders :: default ( ) ) ;
829859 }
830860
831- let mut cb = fake_broken_link_callback;
832-
833861 check_for_code_clusters (
834862 cx,
835863 pulldown_cmark:: Parser :: new_with_broken_link_callback (
836864 & doc,
837865 main_body_opts ( ) - Options :: ENABLE_SMART_PUNCTUATION ,
838- Some ( & mut cb ) ,
866+ Some ( & mut fake_broken_link_callback ) ,
839867 )
840868 . into_offset_iter ( ) ,
841869 & doc,
@@ -845,9 +873,17 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
845873 } ,
846874 ) ;
847875
876+ // NOTE: check_doc uses it own cb function,
877+ // to avoid causing duplicated diagnostics for the broken link checker.
878+ let mut full_fake_broken_link_callback = |bl : BrokenLink < ' _ > | -> Option < ( CowStr < ' _ > , CowStr < ' _ > ) > {
879+ broken_link:: check ( cx, & bl, & doc, & fragments) ;
880+ Some ( ( "fake" . into ( ) , "fake" . into ( ) ) )
881+ } ;
882+
848883 // disable smart punctuation to pick up ['link'] more easily
849884 let opts = main_body_opts ( ) - Options :: ENABLE_SMART_PUNCTUATION ;
850- let parser = pulldown_cmark:: Parser :: new_with_broken_link_callback ( & doc, opts, Some ( & mut cb) ) ;
885+ let parser =
886+ pulldown_cmark:: Parser :: new_with_broken_link_callback ( & doc, opts, Some ( & mut full_fake_broken_link_callback) ) ;
851887
852888 Some ( check_doc (
853889 cx,
0 commit comments