Skip to content

Commit ee10d99

Browse files
committed
generalize markdown to source span calculation
1 parent 03acbd7 commit ee10d99

File tree

2 files changed

+87
-59
lines changed

2 files changed

+87
-59
lines changed

src/librustdoc/passes/collect_intra_doc_links.rs

+4-59
Original file line numberDiff line numberDiff line change
@@ -451,17 +451,9 @@ pub fn span_of_attrs(attrs: &Attributes) -> syntax_pos::Span {
451451

452452
/// Reports a resolution failure diagnostic.
453453
///
454-
/// Ideally we can report the diagnostic with the actual span in the source where the link failure
455-
/// occurred. However, there's a mismatch between the span in the source code and the span in the
456-
/// markdown, so we have to do a bit of work to figure out the correspondence.
457-
///
458-
/// It's not too hard to find the span for sugared doc comments (`///` and `/**`), because the
459-
/// source will match the markdown exactly, excluding the comment markers. However, it's much more
460-
/// difficult to calculate the spans for unsugared docs, because we have to deal with escaping and
461-
/// other source features. So, we attempt to find the exact source span of the resolution failure
462-
/// in sugared docs, but use the span of the documentation attributes themselves for unsugared
463-
/// docs. Because this span might be overly large, we display the markdown line containing the
464-
/// failure as a note.
454+
/// If we cannot find the exact source span of the resolution failure, we use the span of the
455+
/// documentation attributes themselves. This is a little heavy-handed, so we display the markdown
456+
/// line containing the failure as a note as well.
465457
fn resolution_failure(
466458
cx: &DocContext,
467459
attrs: &Attributes,
@@ -473,54 +465,7 @@ fn resolution_failure(
473465
let msg = format!("`[{}]` cannot be resolved, ignoring it...", path_str);
474466

475467
let mut diag = if let Some(link_range) = link_range {
476-
let src = cx.sess().source_map().span_to_snippet(sp);
477-
let is_all_sugared_doc = attrs.doc_strings.iter().all(|frag| match frag {
478-
DocFragment::SugaredDoc(..) => true,
479-
_ => false,
480-
});
481-
482-
if let (Ok(src), true) = (src, is_all_sugared_doc) {
483-
// The number of markdown lines up to and including the resolution failure.
484-
let num_lines = dox[..link_range.start].lines().count();
485-
486-
// We use `split_terminator('\n')` instead of `lines()` when counting bytes to ensure
487-
// that DOS-style line endings do not cause the spans to be calculated incorrectly.
488-
let mut src_lines = src.split_terminator('\n');
489-
let mut md_lines = dox.split_terminator('\n').take(num_lines).peekable();
490-
491-
// The number of bytes from the start of the source span to the resolution failure that
492-
// are *not* part of the markdown, like comment markers.
493-
let mut extra_src_bytes = 0;
494-
495-
while let Some(md_line) = md_lines.next() {
496-
loop {
497-
let source_line = src_lines
498-
.next()
499-
.expect("could not find markdown line in source");
500-
501-
match source_line.find(md_line) {
502-
Some(offset) => {
503-
extra_src_bytes += if md_lines.peek().is_some() {
504-
source_line.len() - md_line.len()
505-
} else {
506-
offset
507-
};
508-
break;
509-
}
510-
None => {
511-
// Since this is a source line that doesn't include a markdown line,
512-
// we have to count the newline that we split from earlier.
513-
extra_src_bytes += source_line.len() + 1;
514-
}
515-
}
516-
}
517-
}
518-
519-
let sp = sp.from_inner_byte_pos(
520-
link_range.start + extra_src_bytes,
521-
link_range.end + extra_src_bytes,
522-
);
523-
468+
if let Some(sp) = super::source_span_for_markdown_range(cx, dox, &link_range, attrs) {
524469
let mut diag = cx.tcx.struct_span_lint_node(
525470
lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE,
526471
NodeId::from_u32(0),

src/librustdoc/passes/mod.rs

+83
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use rustc::util::nodemap::DefIdSet;
88
use std::mem;
99
use std::fmt;
1010
use syntax::ast::NodeId;
11+
use syntax_pos::Span;
12+
use std::ops::Range;
1113

1214
use clean::{self, GetDefId, Item};
1315
use core::{DocContext, DocAccessLevels};
@@ -396,3 +398,84 @@ pub fn look_for_tests<'a, 'tcx: 'a, 'rcx: 'a>(
396398
}
397399
}
398400
}
401+
402+
/// Attempts to match a range of bytes from parsed markdown to a `Span` in the source code.
403+
///
404+
/// This method will return `None` if we cannot construct a span from the source map or if the
405+
/// attributes are not all sugared doc comments. It's difficult to calculate the correct span in
406+
/// that case due to escaping and other source features.
407+
crate fn source_span_for_markdown_range(
408+
cx: &DocContext,
409+
markdown: &str,
410+
md_range: &Range<usize>,
411+
attrs: &clean::Attributes,
412+
) -> Option<Span> {
413+
let is_all_sugared_doc = attrs.doc_strings.iter().all(|frag| match frag {
414+
clean::DocFragment::SugaredDoc(..) => true,
415+
_ => false,
416+
});
417+
418+
if !is_all_sugared_doc {
419+
return None;
420+
}
421+
422+
let snippet = cx
423+
.sess()
424+
.source_map()
425+
.span_to_snippet(span_of_attrs(attrs))
426+
.ok()?;
427+
428+
let starting_line = markdown[..md_range.start].lines().count() - 1;
429+
let ending_line = markdown[..md_range.end].lines().count() - 1;
430+
431+
// We use `split_terminator('\n')` instead of `lines()` when counting bytes so that we only
432+
// we can treat CRLF and LF line endings the same way.
433+
let mut src_lines = snippet.split_terminator('\n');
434+
let md_lines = markdown.split_terminator('\n');
435+
436+
// The number of bytes from the source span to the markdown span that are not part
437+
// of the markdown, like comment markers.
438+
let mut start_bytes = 0;
439+
let mut end_bytes = 0;
440+
441+
'outer: for (line_no, md_line) in md_lines.enumerate() {
442+
loop {
443+
let source_line = src_lines.next().expect("could not find markdown in source");
444+
match source_line.find(md_line) {
445+
Some(offset) => {
446+
if line_no == starting_line {
447+
start_bytes += offset;
448+
449+
if starting_line == ending_line {
450+
break 'outer;
451+
}
452+
} else if line_no == ending_line {
453+
end_bytes += offset;
454+
break 'outer;
455+
} else if line_no < starting_line {
456+
start_bytes += source_line.len() - md_line.len();
457+
} else {
458+
end_bytes += source_line.len() - md_line.len();
459+
}
460+
break;
461+
}
462+
None => {
463+
// Since this is a source line that doesn't include a markdown line,
464+
// we have to count the newline that we split from earlier.
465+
if line_no <= starting_line {
466+
start_bytes += source_line.len() + 1;
467+
} else {
468+
end_bytes += source_line.len() + 1;
469+
}
470+
}
471+
}
472+
}
473+
}
474+
475+
let sp = span_of_attrs(attrs).from_inner_byte_pos(
476+
md_range.start + start_bytes,
477+
md_range.end + start_bytes + end_bytes,
478+
);
479+
480+
Some(sp)
481+
}

0 commit comments

Comments
 (0)