1use std::mem;
2use std::ops::Range;
34use itertools::Itertools;
5/// Re-export the markdown parser used by rustdoc.
6pub use pulldown_cmark;
7use pulldown_cmark::{
8BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, Options, Parser, Tag,
9};
10use rustc_astas ast;
11use rustc_ast::attr::AttributeExt;
12use rustc_ast::join_path_syms;
13use rustc_ast::token::DocFragmentKind;
14use rustc_ast::util::comments::beautify_doc_string;
15use rustc_data_structures::fx::FxIndexMap;
16use rustc_data_structures::unord::UnordSet;
17use rustc_middle::ty::TyCtxt;
18use rustc_span::def_id::DefId;
19use rustc_span::source_map::SourceMap;
20use rustc_span::{DUMMY_SP, InnerSpan, Span, Symbol, sym};
21use thin_vec::ThinVec;
22use tracing::{debug, trace};
2324#[cfg(test)]
25mod tests;
2627/// A portion of documentation, extracted from a `#[doc]` attribute.
28///
29/// Each variant contains the line number within the complete doc-comment where the fragment
30/// starts, as well as the Span where the corresponding doc comment or attribute is located.
31///
32/// Included files are kept separate from inline doc comments so that proper line-number
33/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are
34/// kept separate because of issue #42760.
35#[derive(#[automatically_derived]
impl ::core::clone::Clone for DocFragment {
#[inline]
fn clone(&self) -> DocFragment {
DocFragment {
span: ::core::clone::Clone::clone(&self.span),
item_id: ::core::clone::Clone::clone(&self.item_id),
doc: ::core::clone::Clone::clone(&self.doc),
kind: ::core::clone::Clone::clone(&self.kind),
indent: ::core::clone::Clone::clone(&self.indent),
from_expansion: ::core::clone::Clone::clone(&self.from_expansion),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for DocFragment {
#[inline]
fn eq(&self, other: &DocFragment) -> bool {
self.from_expansion == other.from_expansion && self.span == other.span
&& self.item_id == other.item_id && self.doc == other.doc &&
self.kind == other.kind && self.indent == other.indent
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for DocFragment {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_receiver_is_total_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Span>;
let _: ::core::cmp::AssertParamIsEq<Option<DefId>>;
let _: ::core::cmp::AssertParamIsEq<Symbol>;
let _: ::core::cmp::AssertParamIsEq<DocFragmentKind>;
let _: ::core::cmp::AssertParamIsEq<usize>;
let _: ::core::cmp::AssertParamIsEq<bool>;
}
}Eq, #[automatically_derived]
impl ::core::fmt::Debug for DocFragment {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["span", "item_id", "doc", "kind", "indent", "from_expansion"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.span, &self.item_id, &self.doc, &self.kind, &self.indent,
&&self.from_expansion];
::core::fmt::Formatter::debug_struct_fields_finish(f, "DocFragment",
names, values)
}
}Debug)]
36pub struct DocFragment {
37pub span: Span,
38/// The item this doc-comment came from.
39 /// Used to determine the scope in which doc links in this fragment are resolved.
40 /// Typically filled for reexport docs when they are merged into the docs of the
41 /// original reexported item.
42 /// If the id is not filled, which happens for the original reexported item, then
43 /// it has to be taken from somewhere else during doc link resolution.
44pub item_id: Option<DefId>,
45pub doc: Symbol,
46pub kind: DocFragmentKind,
47pub indent: usize,
48/// Because we tamper with the spans context, this information cannot be correctly retrieved
49 /// later on. So instead, we compute it and store it here.
50pub from_expansion: bool,
51}
5253#[derive(#[automatically_derived]
impl ::core::clone::Clone for MalformedGenerics {
#[inline]
fn clone(&self) -> MalformedGenerics { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for MalformedGenerics { }Copy, #[automatically_derived]
impl ::core::fmt::Debug for MalformedGenerics {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
MalformedGenerics::UnbalancedAngleBrackets =>
"UnbalancedAngleBrackets",
MalformedGenerics::MissingType => "MissingType",
MalformedGenerics::HasFullyQualifiedSyntax =>
"HasFullyQualifiedSyntax",
MalformedGenerics::InvalidPathSeparator =>
"InvalidPathSeparator",
MalformedGenerics::TooManyAngleBrackets =>
"TooManyAngleBrackets",
MalformedGenerics::EmptyAngleBrackets => "EmptyAngleBrackets",
})
}
}Debug)]
54pub enum MalformedGenerics {
55/// This link has unbalanced angle brackets.
56 ///
57 /// For example, `Vec<T` should trigger this, as should `Vec<T>>`.
58UnbalancedAngleBrackets,
59/// The generics are not attached to a type.
60 ///
61 /// For example, `<T>` should trigger this.
62 ///
63 /// This is detected by checking if the path is empty after the generics are stripped.
64MissingType,
65/// The link uses fully-qualified syntax, which is currently unsupported.
66 ///
67 /// For example, `<Vec as IntoIterator>::into_iter` should trigger this.
68 ///
69 /// This is detected by checking if ` as ` (the keyword `as` with spaces around it) is inside
70 /// angle brackets.
71HasFullyQualifiedSyntax,
72/// The link has an invalid path separator.
73 ///
74 /// For example, `Vec:<T>:new()` should trigger this. Note that `Vec:new()` will **not**
75 /// trigger this because it has no generics and thus [`strip_generics_from_path`] will not be
76 /// called.
77 ///
78 /// Note that this will also **not** be triggered if the invalid path separator is inside angle
79 /// brackets because rustdoc mostly ignores what's inside angle brackets (except for
80 /// [`HasFullyQualifiedSyntax`](MalformedGenerics::HasFullyQualifiedSyntax)).
81 ///
82 /// This is detected by checking if there is a colon followed by a non-colon in the link.
83InvalidPathSeparator,
84/// The link has too many angle brackets.
85 ///
86 /// For example, `Vec<<T>>` should trigger this.
87TooManyAngleBrackets,
88/// The link has empty angle brackets.
89 ///
90 /// For example, `Vec<>` should trigger this.
91EmptyAngleBrackets,
92}
9394/// Removes excess indentation on comments in order for the Markdown
95/// to be parsed correctly. This is necessary because the convention for
96/// writing documentation is to provide a space between the /// or //! marker
97/// and the doc text, but Markdown is whitespace-sensitive. For example,
98/// a block of text with four-space indentation is parsed as a code block,
99/// so if we didn't unindent comments, these list items
100///
101/// /// A list:
102/// ///
103/// /// - Foo
104/// /// - Bar
105///
106/// would be parsed as if they were in a code block, which is likely not what the user intended.
107pub fn unindent_doc_fragments(docs: &mut [DocFragment]) {
108// `add` is used in case the most common sugared doc syntax is used ("/// "). The other
109 // fragments kind's lines are never starting with a whitespace unless they are using some
110 // markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
111 // we need to take into account the fact that the minimum indent minus one (to take this
112 // whitespace into account).
113 //
114 // For example:
115 //
116 // /// hello!
117 // #[doc = "another"]
118 //
119 // In this case, you want "hello! another" and not "hello! another".
120let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
121 && docs.iter().any(|d| d.kind.is_sugared())
122 {
123// In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
124 // "decide" how much the minimum indent will be.
1251
126} else {
1270
128};
129130// `min_indent` is used to know how much whitespaces from the start of each lines must be
131 // removed. Example:
132 //
133 // /// hello!
134 // #[doc = "another"]
135 //
136 // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
137 // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
138 // (5 - 1) whitespaces.
139let Some(min_indent) = docs140 .iter()
141 .map(|fragment| {
142fragment143 .doc
144 .as_str()
145 .lines()
146 .filter(|line| line.chars().any(|c| !c.is_whitespace()))
147 .map(|line| {
148// Compare against either space or tab, ignoring whether they are
149 // mixed or not.
150let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
151whitespace + (if fragment.kind.is_sugared() { 0 } else { add })
152 })
153 .min()
154 .unwrap_or(usize::MAX)
155 })
156 .min()
157else {
158return;
159 };
160161for fragment in docs {
162if fragment.doc == sym::empty {
163continue;
164 }
165166let indent = if !fragment.kind.is_sugared() && min_indent > 0 {
167 min_indent - add
168 } else {
169 min_indent
170 };
171172 fragment.indent = indent;
173 }
174}
175176/// The goal of this function is to apply the `DocFragment` transformation that is required when
177/// transforming into the final Markdown, which is applying the computed indent to each line in
178/// each doc fragment (a `DocFragment` can contain multiple lines in case of `#[doc = ""]`).
179///
180/// Note: remove the trailing newline where appropriate
181pub fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
182if frag.doc == sym::empty {
183out.push('\n');
184return;
185 }
186let s = frag.doc.as_str();
187let mut iter = s.lines();
188189while let Some(line) = iter.next() {
190if line.chars().any(|c| !c.is_whitespace()) {
191if !(line.len() >= frag.indent) {
::core::panicking::panic("assertion failed: line.len() >= frag.indent")
};assert!(line.len() >= frag.indent);
192 out.push_str(&line[frag.indent..]);
193 } else {
194 out.push_str(line);
195 }
196 out.push('\n');
197 }
198}
199200pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>(
201 attrs: impl Iterator<Item = (&'a A, Option<DefId>)>,
202 doc_only: bool,
203) -> (Vec<DocFragment>, ThinVec<A>) {
204let (min_size, max_size) = attrs.size_hint();
205let size_hint = max_size.unwrap_or(min_size);
206let mut doc_fragments = Vec::with_capacity(size_hint);
207let mut other_attrs = ThinVec::<A>::with_capacity(if doc_only { 0 } else { size_hint });
208for (attr, item_id) in attrs {
209if let Some((doc_str, fragment_kind)) = attr.doc_str_and_fragment_kind() {
210let doc = beautify_doc_string(doc_str, fragment_kind.comment_kind());
211let attr_span = attr.span();
212let (span, from_expansion) = match fragment_kind {
213 DocFragmentKind::Sugared(_) => (attr_span, attr_span.from_expansion()),
214 DocFragmentKind::Raw(value_span) => {
215 (value_span.with_ctxt(attr_span.ctxt()), value_span.from_expansion())
216 }
217 };
218let fragment =
219 DocFragment { span, doc, kind: fragment_kind, item_id, indent: 0, from_expansion };
220 doc_fragments.push(fragment);
221 } else if !doc_only {
222 other_attrs.push(attr.clone());
223 }
224 }
225226doc_fragments.shrink_to_fit();
227other_attrs.shrink_to_fit();
228229unindent_doc_fragments(&mut doc_fragments);
230231 (doc_fragments, other_attrs)
232}
233234/// Return the doc-comments on this item, grouped by the module they came from.
235/// The module can be different if this is a re-export with added documentation.
236///
237/// The last newline is not trimmed so the produced strings are reusable between
238/// early and late doc link resolution regardless of their position.
239pub fn prepare_to_doc_link_resolution(
240 doc_fragments: &[DocFragment],
241) -> FxIndexMap<Option<DefId>, String> {
242let mut res = FxIndexMap::default();
243for fragment in doc_fragments {
244let out_str = res.entry(fragment.item_id).or_default();
245 add_doc_fragment(out_str, fragment);
246 }
247res248}
249250/// Options for rendering Markdown in the main body of documentation.
251pub fn main_body_opts() -> Options {
252Options::ENABLE_TABLES253 | Options::ENABLE_FOOTNOTES254 | Options::ENABLE_STRIKETHROUGH255 | Options::ENABLE_TASKLISTS256 | Options::ENABLE_SMART_PUNCTUATION257}
258259fn strip_generics_from_path_segment(segment: Vec<char>) -> Result<Symbol, MalformedGenerics> {
260let mut stripped_segment = String::new();
261let mut param_depth = 0;
262263let mut latest_generics_chunk = String::new();
264265for c in segment {
266if c == '<' {
267 param_depth += 1;
268 latest_generics_chunk.clear();
269 } else if c == '>' {
270 param_depth -= 1;
271if latest_generics_chunk.contains(" as ") {
272// The segment tries to use fully-qualified syntax, which is currently unsupported.
273 // Give a helpful error message instead of completely ignoring the angle brackets.
274return Err(MalformedGenerics::HasFullyQualifiedSyntax);
275 }
276 } else if param_depth == 0 {
277 stripped_segment.push(c);
278 } else {
279 latest_generics_chunk.push(c);
280 }
281 }
282283if param_depth == 0 {
284Ok(Symbol::intern(&stripped_segment))
285 } else {
286// The segment has unbalanced angle brackets, e.g. `Vec<T` or `Vec<T>>`
287Err(MalformedGenerics::UnbalancedAngleBrackets)
288 }
289}
290291pub fn strip_generics_from_path(path_str: &str) -> Result<Box<str>, MalformedGenerics> {
292if !path_str.contains(['<', '>']) {
293return Ok(path_str.into());
294 }
295let mut stripped_segments = ::alloc::vec::Vec::new()vec![];
296let mut path = path_str.chars().peekable();
297let mut segment = Vec::new();
298299while let Some(chr) = path.next() {
300match chr {
301':' => {
302if path.next_if_eq(&':').is_some() {
303let stripped_segment =
304 strip_generics_from_path_segment(mem::take(&mut segment))?;
305if !stripped_segment.is_empty() {
306 stripped_segments.push(stripped_segment);
307 }
308 } else {
309return Err(MalformedGenerics::InvalidPathSeparator);
310 }
311 }
312'<' => {
313 segment.push(chr);
314315match path.next() {
316Some('<') => {
317return Err(MalformedGenerics::TooManyAngleBrackets);
318 }
319Some('>') => {
320return Err(MalformedGenerics::EmptyAngleBrackets);
321 }
322Some(chr) => {
323 segment.push(chr);
324325while let Some(chr) = path.next_if(|c| *c != '>') {
326 segment.push(chr);
327 }
328 }
329None => break,
330 }
331 }
332_ => segment.push(chr),
333 }
334{
use ::tracing::__macro_support::Callsite as _;
static __CALLSITE: ::tracing::callsite::DefaultCallsite =
{
static META: ::tracing::Metadata<'static> =
{
::tracing_core::metadata::Metadata::new("event compiler/rustc_resolve/src/rustdoc.rs:334",
"rustc_resolve::rustdoc", ::tracing::Level::TRACE,
::tracing_core::__macro_support::Option::Some("compiler/rustc_resolve/src/rustdoc.rs"),
::tracing_core::__macro_support::Option::Some(334u32),
::tracing_core::__macro_support::Option::Some("rustc_resolve::rustdoc"),
::tracing_core::field::FieldSet::new(&["message"],
::tracing_core::callsite::Identifier(&__CALLSITE)),
::tracing::metadata::Kind::EVENT)
};
::tracing::callsite::DefaultCallsite::new(&META)
};
let enabled =
::tracing::Level::TRACE <= ::tracing::level_filters::STATIC_MAX_LEVEL
&&
::tracing::Level::TRACE <=
::tracing::level_filters::LevelFilter::current() &&
{
let interest = __CALLSITE.interest();
!interest.is_never() &&
::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
interest)
};
if enabled {
(|value_set: ::tracing::field::ValueSet|
{
let meta = __CALLSITE.metadata();
::tracing::Event::dispatch(meta, &value_set);
;
})({
#[allow(unused_imports)]
use ::tracing::field::{debug, display, Value};
let mut iter = __CALLSITE.metadata().fields().iter();
__CALLSITE.metadata().fields().value_set(&[(&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
::tracing::__macro_support::Option::Some(&format_args!("raw segment: {0:?}",
segment) as &dyn Value))])
});
} else { ; }
};trace!("raw segment: {:?}", segment);
335 }
336337if !segment.is_empty() {
338let stripped_segment = strip_generics_from_path_segment(segment)?;
339if !stripped_segment.is_empty() {
340stripped_segments.push(stripped_segment);
341 }
342 }
343344{
use ::tracing::__macro_support::Callsite as _;
static __CALLSITE: ::tracing::callsite::DefaultCallsite =
{
static META: ::tracing::Metadata<'static> =
{
::tracing_core::metadata::Metadata::new("event compiler/rustc_resolve/src/rustdoc.rs:344",
"rustc_resolve::rustdoc", ::tracing::Level::DEBUG,
::tracing_core::__macro_support::Option::Some("compiler/rustc_resolve/src/rustdoc.rs"),
::tracing_core::__macro_support::Option::Some(344u32),
::tracing_core::__macro_support::Option::Some("rustc_resolve::rustdoc"),
::tracing_core::field::FieldSet::new(&["message"],
::tracing_core::callsite::Identifier(&__CALLSITE)),
::tracing::metadata::Kind::EVENT)
};
::tracing::callsite::DefaultCallsite::new(&META)
};
let enabled =
::tracing::Level::DEBUG <= ::tracing::level_filters::STATIC_MAX_LEVEL
&&
::tracing::Level::DEBUG <=
::tracing::level_filters::LevelFilter::current() &&
{
let interest = __CALLSITE.interest();
!interest.is_never() &&
::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
interest)
};
if enabled {
(|value_set: ::tracing::field::ValueSet|
{
let meta = __CALLSITE.metadata();
::tracing::Event::dispatch(meta, &value_set);
;
})({
#[allow(unused_imports)]
use ::tracing::field::{debug, display, Value};
let mut iter = __CALLSITE.metadata().fields().iter();
__CALLSITE.metadata().fields().value_set(&[(&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
::tracing::__macro_support::Option::Some(&format_args!("path_str: {0:?}\nstripped segments: {1:?}",
path_str, stripped_segments) as &dyn Value))])
});
} else { ; }
};debug!("path_str: {path_str:?}\nstripped segments: {stripped_segments:?}");
345346if !stripped_segments.is_empty() {
347let stripped_path = join_path_syms(stripped_segments);
348Ok(stripped_path.into())
349 } else {
350Err(MalformedGenerics::MissingType)
351 }
352}
353354/// Returns whether the first doc-comment is an inner attribute.
355///
356/// If there are no doc-comments, return true.
357/// FIXME(#78591): Support both inner and outer attributes on the same item.
358pub fn inner_docs(attrs: &[impl AttributeExt]) -> bool {
359for attr in attrs {
360if let Some(attr_style) = attr.doc_resolution_scope() {
361return attr_style == ast::AttrStyle::Inner;
362 }
363 }
364true
365}
366367/// Has `#[rustc_doc_primitive]` or `#[doc(keyword)]` or `#[doc(attribute)]`.
368pub fn has_primitive_or_keyword_or_attribute_docs(attrs: &[impl AttributeExt]) -> bool {
369for attr in attrs {
370if attr.has_name(sym::rustc_doc_primitive) || attr.is_doc_keyword_or_attribute() {
371return true;
372 }
373 }
374false
375}
376377/// Simplified version of the corresponding function in rustdoc.
378fn preprocess_link(link: &str) -> Box<str> {
379// IMPORTANT: To be kept in sync with the corresponding function in rustdoc.
380 // Namely, whenever the rustdoc function returns a successful result for a given input,
381 // this function *MUST* return a link that's equal to `PreprocessingInfo.path_str`!
382383let link = link.replace('`', "");
384let link = link.split('#').next().unwrap();
385let link = link.trim();
386let link = link.split_once('@').map_or(link, |(_, rhs)| rhs);
387let link = link.trim_suffix("()");
388let link = link.trim_suffix("{}");
389let link = link.trim_suffix("[]");
390let link = if link != "!" { link.trim_suffix('!') } else { link };
391let link = link.trim();
392strip_generics_from_path(link).unwrap_or_else(|_| link.into())
393}
394395/// Keep inline and reference links `[]`,
396/// but skip autolinks `<>` which we never consider to be intra-doc links.
397pub fn may_be_doc_link(link_type: LinkType) -> bool {
398match link_type {
399 LinkType::Inline400 | LinkType::Reference401 | LinkType::ReferenceUnknown402 | LinkType::Collapsed403 | LinkType::CollapsedUnknown404 | LinkType::Shortcut405 | LinkType::ShortcutUnknown => true,
406 LinkType::Autolink | LinkType::Email => false,
407 }
408}
409410/// Simplified version of `preprocessed_markdown_links` from rustdoc.
411/// Must return at least the same links as it, but may add some more links on top of that.
412pub(crate) fn attrs_to_preprocessed_links<A: AttributeExt + Clone>(attrs: &[A]) -> Vec<Box<str>> {
413let (doc_fragments, other_attrs) =
414attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), false);
415let mut doc =
416prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap_or_default();
417418for attr in other_attrs {
419if let Some(note) = attr.deprecation_note() {
420 doc += note.as_str();
421 doc += "\n";
422 }
423 }
424425parse_links(&doc)
426}
427428/// Similar version of `markdown_links` from rustdoc.
429/// This will collect destination links and display text if exists.
430fn parse_links<'md>(doc: &'md str) -> Vec<Box<str>> {
431let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
432let mut event_iter = Parser::new_with_broken_link_callback(
433doc,
434main_body_opts(),
435Some(&mut broken_link_callback),
436 );
437let mut links = Vec::new();
438439let mut refids = UnordSet::default();
440441while let Some(event) = event_iter.next() {
442match event {
443 Event::Start(Tag::Link { link_type, dest_url, title: _, id })
444if may_be_doc_link(link_type) =>
445 {
446if #[allow(non_exhaustive_omitted_patterns)] match link_type {
LinkType::Inline | LinkType::ReferenceUnknown | LinkType::Reference |
LinkType::Shortcut | LinkType::ShortcutUnknown => true,
_ => false,
}matches!(
447 link_type,
448 LinkType::Inline
449 | LinkType::ReferenceUnknown
450 | LinkType::Reference
451 | LinkType::Shortcut
452 | LinkType::ShortcutUnknown
453 ) {
454if let Some(display_text) = collect_link_data(&mut event_iter) {
455 links.push(display_text);
456 }
457 }
458if #[allow(non_exhaustive_omitted_patterns)] match link_type {
LinkType::Reference | LinkType::Shortcut | LinkType::Collapsed => true,
_ => false,
}matches!(
459 link_type,
460 LinkType::Reference | LinkType::Shortcut | LinkType::Collapsed
461 ) {
462 refids.insert(id);
463 }
464465 links.push(preprocess_link(&dest_url));
466 }
467_ => {}
468 }
469 }
470471for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
472if !refids.contains(label) {
473 links.push(preprocess_link(&refdef.dest));
474 }
475 }
476477links478}
479480/// Collects additional data of link.
481fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
482 event_iter: &mut Parser<'input, F>,
483) -> Option<Box<str>> {
484let mut display_text: Option<String> = None;
485let mut append_text = |text: CowStr<'_>| {
486if let Some(display_text) = &mut display_text {
487display_text.push_str(&text);
488 } else {
489display_text = Some(text.to_string());
490 }
491 };
492493while let Some(event) = event_iter.next() {
494match event {
495 Event::Text(text) => {
496 append_text(text);
497 }
498 Event::Code(code) => {
499 append_text(code);
500 }
501 Event::End(_) => {
502break;
503 }
504_ => {}
505 }
506 }
507508display_text.map(String::into_boxed_str)
509}
510511/// Returns a span encompassing all the document fragments.
512pub fn span_of_fragments(fragments: &[DocFragment]) -> Option<Span> {
513let (first_fragment, last_fragment) = match fragments {
514 [] => return None,
515 [first, .., last] => (first, last),
516 [first] => (first, first),
517 };
518if first_fragment.span == DUMMY_SP {
519return None;
520 }
521Some(first_fragment.span.to(last_fragment.span))
522}
523524/// Attempts to match a range of bytes from parsed markdown to a `Span` in the source code.
525///
526/// This method does not always work, because markdown bytes don't necessarily match source bytes,
527/// like if escapes are used in the string. In this case, it returns `None`.
528///
529/// `markdown` is typically the entire documentation for an item,
530/// after combining fragments.
531///
532/// This method will return `Some` only if one of the following is true:
533///
534/// - The doc is made entirely from sugared doc comments, which cannot contain escapes
535/// - The doc is entirely from a single doc fragment with a string literal exactly equal to
536/// `markdown`.
537/// - The doc comes from `include_str!`
538/// - The doc includes exactly one substring matching `markdown[md_range]` which is contained in a
539/// single doc fragment.
540///
541/// This function is defined in the compiler so it can be used by both `rustdoc` and `clippy`.
542///
543/// It returns a tuple containing a span encompassing all the document fragments and a boolean that
544/// is `true` if any of the *matched* fragments are from a macro expansion.
545pub fn source_span_for_markdown_range(
546 tcx: TyCtxt<'_>,
547 markdown: &str,
548 md_range: &Range<usize>,
549 fragments: &[DocFragment],
550) -> Option<(Span, bool)> {
551let map = tcx.sess.source_map();
552source_span_for_markdown_range_inner(map, markdown, md_range, fragments)
553}
554555// inner function used for unit testing
556pub fn source_span_for_markdown_range_inner(
557 map: &SourceMap,
558 markdown: &str,
559 md_range: &Range<usize>,
560 fragments: &[DocFragment],
561) -> Option<(Span, bool)> {
562use rustc_span::BytePos;
563564if let &[fragment] = &fragments565 && !fragment.kind.is_sugared()
566 && let Ok(snippet) = map.span_to_snippet(fragment.span)
567 && snippet.trim_end() == markdown.trim_end()
568 && let Ok(md_range_lo) = u32::try_from(md_range.start)
569 && let Ok(md_range_hi) = u32::try_from(md_range.end)
570 {
571// Single fragment with string that contains same bytes as doc.
572return Some((
573Span::new(
574fragment.span.lo() + rustc_span::BytePos(md_range_lo),
575fragment.span.lo() + rustc_span::BytePos(md_range_hi),
576fragment.span.ctxt(),
577fragment.span.parent(),
578 ),
579fragment.from_expansion,
580 ));
581 }
582583let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind.is_sugared());
584585if !is_all_sugared_doc {
586// This case ignores the markdown outside of the range so that it can
587 // work in cases where the markdown is made from several different
588 // doc fragments, but the target range does not span across multiple
589 // fragments.
590let mut match_data = None;
591let pat = &markdown[md_range.clone()];
592// This heirustic doesn't make sense with a zero-sized range.
593if pat.is_empty() {
594return None;
595 }
596for (i, fragment) in fragments.iter().enumerate() {
597if let Ok(snippet) = map.span_to_snippet(fragment.span)
598 && let Some(match_start) = snippet.find(pat)
599 {
600// If there is either a match in a previous fragment, or
601 // multiple matches in this fragment, there is ambiguity.
602 // the snippet cannot be zero-sized, because it matches
603 // the pattern, which is checked to not be zero sized.
604if match_data.is_none()
605 && !snippet.as_bytes()[match_start + 1..]
606 .windows(pat.len())
607 .any(|s| s == pat.as_bytes())
608 {
609 match_data = Some((i, match_start));
610 } else {
611// Heuristic produced ambiguity, return nothing.
612return None;
613 }
614 }
615 }
616if let Some((i, match_start)) = match_data {
617let fragment = &fragments[i];
618let sp = fragment.span;
619// we need to calculate the span start,
620 // then use that in our calculations for the span end
621let lo = sp.lo() + BytePos(match_startas u32);
622return Some((
623sp.with_lo(lo).with_hi(lo + BytePos((md_range.end - md_range.start) as u32)),
624fragment.from_expansion,
625 ));
626 }
627return None;
628 }
629630let snippet = map.span_to_snippet(span_of_fragments(fragments)?).ok()?;
631632let starting_line = markdown[..md_range.start].matches('\n').count();
633let ending_line = starting_line + markdown[md_range.start..md_range.end].matches('\n').count();
634635// We use `split_terminator('\n')` instead of `lines()` when counting bytes so that we treat
636 // CRLF and LF line endings the same way.
637let mut src_lines = snippet.split_terminator('\n');
638let md_lines = markdown.split_terminator('\n');
639640// The number of bytes from the source span to the markdown span that are not part
641 // of the markdown, like comment markers.
642let mut start_bytes = 0;
643let mut end_bytes = 0;
644645'outer: for (line_no, md_line) in md_lines.enumerate() {
646loop {
647let source_line = src_lines.next()?;
648match source_line.find(md_line) {
649Some(offset) => {
650if line_no == starting_line {
651 start_bytes += offset;
652653if starting_line == ending_line {
654break 'outer;
655 }
656 } else if line_no == ending_line {
657 end_bytes += offset;
658break 'outer;
659 } else if line_no < starting_line {
660 start_bytes += source_line.len() - md_line.len();
661 } else {
662 end_bytes += source_line.len() - md_line.len();
663 }
664break;
665 }
666None => {
667// Since this is a source line that doesn't include a markdown line,
668 // we have to count the newline that we split from earlier.
669if line_no <= starting_line {
670 start_bytes += source_line.len() + 1;
671 } else {
672 end_bytes += source_line.len() + 1;
673 }
674 }
675 }
676 }
677 }
678679let span = span_of_fragments(fragments)?;
680let src_span = span.from_inner(InnerSpan::new(
681md_range.start + start_bytes,
682md_range.end + start_bytes + end_bytes,
683 ));
684Some((
685src_span,
686fragments.iter().any(|frag| frag.span.overlaps(src_span) && frag.from_expansion),
687 ))
688}