@@ -17,7 +17,10 @@ use crate::core::DocContext;
1717use crate :: html:: markdown:: main_body_opts;
1818
1919pub ( super ) fn visit_item ( cx : & DocContext < ' _ > , item : & Item , hir_id : HirId , dox : & str ) {
20- let report_diag = |cx : & DocContext < ' _ > , msg : & ' static str , range : Range < usize > | {
20+ let report_diag = |cx : & DocContext < ' _ > ,
21+ msg : & ' static str ,
22+ range : Range < usize > ,
23+ without_brackets : Option < & str > | {
2124 let maybe_sp = source_span_for_markdown_range ( cx. tcx , dox, & range, & item. attrs . doc_strings )
2225 . map ( |( sp, _) | sp) ;
2326 let sp = maybe_sp. unwrap_or_else ( || item. attr_span ( cx. tcx ) ) ;
@@ -27,14 +30,22 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
2730 // The fallback of using the attribute span is suitable for
2831 // highlighting where the error is, but not for placing the < and >
2932 if let Some ( sp) = maybe_sp {
30- lint. multipart_suggestion (
31- "use an automatic link instead" ,
32- vec ! [
33- ( sp. shrink_to_lo( ) , "<" . to_string( ) ) ,
34- ( sp. shrink_to_hi( ) , ">" . to_string( ) ) ,
35- ] ,
36- Applicability :: MachineApplicable ,
37- ) ;
33+ if let Some ( without_brackets) = without_brackets {
34+ lint. multipart_suggestion (
35+ "use an automatic link instead" ,
36+ vec ! [ ( sp, format!( "<{without_brackets}>" ) ) ] ,
37+ Applicability :: MachineApplicable ,
38+ ) ;
39+ } else {
40+ lint. multipart_suggestion (
41+ "use an automatic link instead" ,
42+ vec ! [
43+ ( sp. shrink_to_lo( ) , "<" . to_string( ) ) ,
44+ ( sp. shrink_to_hi( ) , ">" . to_string( ) ) ,
45+ ] ,
46+ Applicability :: MachineApplicable ,
47+ ) ;
48+ }
3849 }
3950 } ) ;
4051 } ;
@@ -43,7 +54,7 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
4354
4455 while let Some ( ( event, range) ) = p. next ( ) {
4556 match event {
46- Event :: Text ( s) => find_raw_urls ( cx, & s, range, & report_diag) ,
57+ Event :: Text ( s) => find_raw_urls ( cx, dox , & s, range, & report_diag) ,
4758 // We don't want to check the text inside code blocks or links.
4859 Event :: Start ( tag @ ( Tag :: CodeBlock ( _) | Tag :: Link { .. } ) ) => {
4960 for ( event, _) in p. by_ref ( ) {
@@ -67,25 +78,35 @@ static URL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
6778 r"https?://" , // url scheme
6879 r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+" , // one or more subdomains
6980 r"[a-zA-Z]{2,63}" , // root domain
70- r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)" // optional query or url fragments
81+ r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)" , // optional query or url fragments
7182 ) )
7283 . expect ( "failed to build regex" )
7384} ) ;
7485
7586fn find_raw_urls (
7687 cx : & DocContext < ' _ > ,
88+ dox : & str ,
7789 text : & str ,
7890 range : Range < usize > ,
79- f : & impl Fn ( & DocContext < ' _ > , & ' static str , Range < usize > ) ,
91+ f : & impl Fn ( & DocContext < ' _ > , & ' static str , Range < usize > , Option < & str > ) ,
8092) {
8193 trace ! ( "looking for raw urls in {text}" ) ;
8294 // For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
8395 for match_ in URL_REGEX . find_iter ( text) {
84- let url_range = match_. range ( ) ;
85- f (
86- cx,
87- "this URL is not a hyperlink" ,
88- Range { start : range. start + url_range. start , end : range. start + url_range. end } ,
89- ) ;
96+ let mut url_range = match_. range ( ) ;
97+ url_range. start += range. start ;
98+ url_range. end += range. start ;
99+ let mut without_brackets = None ;
100+ // If the link is contained inside `[]`, then we need to replace the brackets and
101+ // not just add `<>`.
102+ if dox[ ..url_range. start ] . ends_with ( '[' )
103+ && url_range. end <= dox. len ( )
104+ && dox[ url_range. end ..] . starts_with ( ']' )
105+ {
106+ url_range. start -= 1 ;
107+ url_range. end += 1 ;
108+ without_brackets = Some ( match_. as_str ( ) ) ;
109+ }
110+ f ( cx, "this URL is not a hyperlink" , url_range, without_brackets) ;
90111 }
91112}
0 commit comments