Skip to content

Commit c802255

Browse files
committed
add another heirustic to source_span_for_markdown_range
1 parent 791f92c commit c802255

File tree

6 files changed

+159
-2
lines changed

6 files changed

+159
-2
lines changed

compiler/rustc_resolve/src/rustdoc.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,20 +498,25 @@ pub fn span_of_fragments(fragments: &[DocFragment]) -> Option<Span> {
498498
/// This method does not always work, because markdown bytes don't necessarily match source bytes,
499499
/// like if escapes are used in the string. In this case, it returns `None`.
500500
///
501+
/// `markdown` is typically the entire documentation for an item,
502+
/// after combining fragments.
503+
///
501504
/// This method will return `Some` only if:
502505
///
503506
/// - The doc is made entirely from sugared doc comments, which cannot contain escapes
504507
/// - The doc is entirely from a single doc fragment, with a string literal, exactly equal
505508
/// - The doc comes from `include_str!`
509+
/// - The doc includes exactly one substring matching `markdown[md_range]` which is contained in a single doc fragment.
506510
pub fn source_span_for_markdown_range(
507511
tcx: TyCtxt<'_>,
508512
markdown: &str,
509513
md_range: &Range<usize>,
510514
fragments: &[DocFragment],
511515
) -> Option<Span> {
516+
let span_to_snippet = |span| tcx.sess.source_map().span_to_snippet(span);
512517
if let &[fragment] = &fragments
513518
&& fragment.kind == DocFragmentKind::RawDoc
514-
&& let Ok(snippet) = tcx.sess.source_map().span_to_snippet(fragment.span)
519+
&& let Ok(snippet) = span_to_snippet(fragment.span)
515520
&& snippet.trim_end() == markdown.trim_end()
516521
&& let Ok(md_range_lo) = u32::try_from(md_range.start)
517522
&& let Ok(md_range_hi) = u32::try_from(md_range.end)
@@ -528,6 +533,30 @@ pub fn source_span_for_markdown_range(
528533
let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind == DocFragmentKind::SugaredDoc);
529534

530535
if !is_all_sugared_doc {
536+
// this case ignores the markdown outside of the range so that it can
537+
// work in cases where the markdown is made from several different
538+
// doc fragments, but the target range does not span across multiple
539+
// fragments.
540+
let mut match_data = None;
541+
for (i, fragment) in fragments.iter().enumerate() {
542+
if let Ok(snippet) = span_to_snippet(fragment.span)
543+
&& let Some(match_start) = snippet.find(&markdown[md_range.clone()])
544+
{
545+
if match_data.is_none() {
546+
match_data = Some((i, match_start));
547+
} else {
548+
// heirustic produced ambiguity, return nothing.
549+
return None;
550+
}
551+
}
552+
}
553+
if let Some((i, match_start)) = match_data {
554+
use rustc_span::BytePos;
555+
let mut sp = fragments[i].span;
556+
sp = sp.with_lo(sp.lo() + BytePos(match_start as u32));
557+
sp = sp.with_hi(sp.lo() + BytePos((md_range.end - md_range.start) as u32));
558+
return Some(sp);
559+
}
531560
return None;
532561
}
533562

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//@ check-fail
2+
3+
#![deny(rustdoc::bare_urls)]
4+
5+
// examples of bare urls that are beyond our ability to generate suggestions for
6+
7+
// this falls through every heirustic in `source_span_for_markdown_range`,
8+
// and thus does not get any suggestion.
9+
#[doc = "good: <https://example.com/> \n\n"]
10+
//~^ ERROR this URL is not a hyperlink
11+
#[doc = "bad: https://example.com/"]
12+
pub fn duplicate_raw() {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error: this URL is not a hyperlink
2+
--> $DIR/bare-urls-limit.rs:9:9
3+
|
4+
LL | #[doc = "good: <https://example.com/> \n\n"]
5+
| _________^
6+
LL | |
7+
LL | | #[doc = "bad: https://example.com/"]
8+
| |___________________________________^
9+
|
10+
= note: bare URLs are not automatically turned into clickable links
11+
note: the lint level is defined here
12+
--> $DIR/bare-urls-limit.rs:3:9
13+
|
14+
LL | #![deny(rustdoc::bare_urls)]
15+
| ^^^^^^^^^^^^^^^^^^
16+
17+
error: aborting due to 1 previous error
18+

tests/rustdoc-ui/lints/bare-urls.fixed

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838
//~^ ERROR this URL is not a hyperlink
3939
pub fn c() {}
4040

41+
#[doc = "here's a thing: <https://example.com/>"]
42+
//~^ ERROR this URL is not a hyperlink
43+
pub fn f() {}
44+
45+
/// <https://example.com/sugar>
46+
//~^ ERROR this URL is not a hyperlink
47+
#[doc = "<https://example.com/raw>"]
48+
//~^ ERROR this URL is not a hyperlink
49+
pub fn mixed() {}
50+
4151
/// <https://somewhere.com>
4252
/// [a](http://a.com)
4353
/// [b]

tests/rustdoc-ui/lints/bare-urls.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838
//~^ ERROR this URL is not a hyperlink
3939
pub fn c() {}
4040

41+
#[doc = "here's a thing: https://example.com/"]
42+
//~^ ERROR this URL is not a hyperlink
43+
pub fn f() {}
44+
45+
/// https://example.com/sugar
46+
//~^ ERROR this URL is not a hyperlink
47+
#[doc = "https://example.com/raw"]
48+
//~^ ERROR this URL is not a hyperlink
49+
pub fn mixed() {}
50+
4151
/// <https://somewhere.com>
4252
/// [a](http://a.com)
4353
/// [b]

0 commit comments

Comments
 (0)