Skip to main content

rustc_resolve/
rustdoc.rs

1use std::mem;
2use std::ops::Range;
3
4use itertools::Itertools;
5/// Re-export the markdown parser used by rustdoc.
6pub use pulldown_cmark;
7use pulldown_cmark::{
8    BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, Options, Parser, Tag,
9};
10use rustc_ast as ast;
11use rustc_ast::attr::AttributeExt;
12use rustc_ast::token::DocFragmentKind;
13use rustc_ast::util::comments::beautify_doc_string;
14use rustc_ast::{AttrKind, AttrStyle, join_path_syms};
15use rustc_attr_ir::{Attribute, AttributeKind, DocAttribute};
16use rustc_data_structures::fx::FxIndexMap;
17use rustc_data_structures::unord::UnordSet;
18use rustc_middle::ty::TyCtxt;
19use rustc_span::def_id::DefId;
20use rustc_span::source_map::SourceMap;
21use rustc_span::{DUMMY_SP, InnerSpan, Span, Symbol, sym};
22use thin_vec::ThinVec;
23use tracing::{debug, trace};
24
25#[cfg(test)]
26mod tests;
27
28/// A portion of documentation, extracted from a `#[doc]` attribute.
29///
30/// Each variant contains the line number within the complete doc-comment where the fragment
31/// starts, as well as the Span where the corresponding doc comment or attribute is located.
32///
33/// Included files are kept separate from inline doc comments so that proper line-number
34/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are
35/// kept separate because of issue #42760.
36#[derive(#[automatically_derived]impl ::core::clone::Clone for DocFragment {
    #[inline]
    #[rustc_trivial_field_reads]
    fn clone(&self) -> Self {
        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),
            style: ::core::clone::Clone::clone(&self.style),
        }
    }
}Clone, #[automatically_derived]impl ::core::marker::StructuralPartialEq for
    DocFragment {
}
#[automatically_derived]impl ::core::cmp::PartialEq for DocFragment {
    #[inline]
    fn eq(&self, other: &Self) -> 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 &&
            self.style == other.style
    }
}PartialEq, #[automatically_derived]impl ::core::cmp::Eq for DocFragment {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_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>;
        let _: ::core::cmp::AssertParamIsEq<AttrStyle>;
    }
}Eq, #[automatically_derived]impl ::core::fmt::Debug for DocFragment {
    #[inline]
    #[rustc_trivial_field_reads]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            "DocFragment span item_id doc kind indent from_expansion style";
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.span, &self.item_id, &self.doc, &self.kind, &self.indent,
                        &self.from_expansion, &&self.style];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, names, values)
    }
}Debug)]
37pub struct DocFragment {
38    pub span: Span,
39    /// The item this doc-comment came from.
40    /// Used to determine the scope in which doc links in this fragment are resolved.
41    /// Typically filled for reexport docs when they are merged into the docs of the
42    /// original reexported item.
43    /// If the id is not filled, which happens for the original reexported item, then
44    /// it has to be taken from somewhere else during doc link resolution.
45    pub item_id: Option<DefId>,
46    pub doc: Symbol,
47    pub kind: DocFragmentKind,
48    pub indent: usize,
49    /// Because we tamper with the spans context, this information cannot be correctly retrieved
50    /// later on. So instead, we compute it and store it here.
51    pub from_expansion: bool,
52    pub style: AttrStyle,
53}
54
55#[derive(#[automatically_derived]#[doc(hidden)]unsafe impl ::core::clone::TrivialClone
    for MalformedGenerics {
}
#[automatically_derived]impl ::core::clone::Clone for MalformedGenerics {
    #[inline]
    #[rustc_trivial_field_reads]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]impl ::core::marker::Copy for MalformedGenerics { }Copy, #[automatically_derived]impl ::core::fmt::Debug for MalformedGenerics {
    #[inline]
    #[rustc_trivial_field_reads]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_c_like_enum_write_str(f,
            "UnbalancedAngleBrackets MissingType HasFullyQualifiedSyntax InvalidPathSeparator TooManyAngleBrackets EmptyAngleBrackets",
            match self {
                MalformedGenerics::UnbalancedAngleBrackets => 0usize,
                MalformedGenerics::MissingType => 24usize,
                MalformedGenerics::HasFullyQualifiedSyntax => 36usize,
                MalformedGenerics::InvalidPathSeparator => 60usize,
                MalformedGenerics::TooManyAngleBrackets => 81usize,
                MalformedGenerics::EmptyAngleBrackets => 102usize,
            })
    }
}Debug)]
56pub enum MalformedGenerics {
57    /// This link has unbalanced angle brackets.
58    ///
59    /// For example, `Vec<T` should trigger this, as should `Vec<T>>`.
60    UnbalancedAngleBrackets,
61    /// The generics are not attached to a type.
62    ///
63    /// For example, `<T>` should trigger this.
64    ///
65    /// This is detected by checking if the path is empty after the generics are stripped.
66    MissingType,
67    /// The link uses fully-qualified syntax, which is currently unsupported.
68    ///
69    /// For example, `<Vec as IntoIterator>::into_iter` should trigger this.
70    ///
71    /// This is detected by checking if ` as ` (the keyword `as` with spaces around it) is inside
72    /// angle brackets.
73    HasFullyQualifiedSyntax,
74    /// The link has an invalid path separator.
75    ///
76    /// For example, `Vec:<T>:new()` should trigger this. Note that `Vec:new()` will **not**
77    /// trigger this because it has no generics and thus [`strip_generics_from_path`] will not be
78    /// called.
79    ///
80    /// Note that this will also **not** be triggered if the invalid path separator is inside angle
81    /// brackets because rustdoc mostly ignores what's inside angle brackets (except for
82    /// [`HasFullyQualifiedSyntax`](MalformedGenerics::HasFullyQualifiedSyntax)).
83    ///
84    /// This is detected by checking if there is a colon followed by a non-colon in the link.
85    InvalidPathSeparator,
86    /// The link has too many angle brackets.
87    ///
88    /// For example, `Vec<<T>>` should trigger this.
89    TooManyAngleBrackets,
90    /// The link has empty angle brackets.
91    ///
92    /// For example, `Vec<>` should trigger this.
93    EmptyAngleBrackets,
94}
95
96/// Removes excess indentation on comments in order for the Markdown
97/// to be parsed correctly. This is necessary because the convention for
98/// writing documentation is to provide a space between the /// or //! marker
99/// and the doc text, but Markdown is whitespace-sensitive. For example,
100/// a block of text with four-space indentation is parsed as a code block,
101/// so if we didn't unindent comments, these list items
102///
103/// /// A list:
104/// ///
105/// ///    - Foo
106/// ///    - Bar
107///
108/// would be parsed as if they were in a code block, which is likely not what the user intended.
109pub fn unindent_doc_fragments(docs: &mut [DocFragment]) {
110    // `add` is used in case the most common sugared doc syntax is used ("/// "). The other
111    // fragments kind's lines are never starting with a whitespace unless they are using some
112    // markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
113    // we need to take into account the fact that the minimum indent minus one (to take this
114    // whitespace into account).
115    //
116    // For example:
117    //
118    // /// hello!
119    // #[doc = "another"]
120    //
121    // In this case, you want "hello! another" and not "hello!  another".
122    let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
123        && docs.iter().any(|d| d.kind.is_sugared())
124    {
125        // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
126        // "decide" how much the minimum indent will be.
127        1
128    } else {
129        0
130    };
131
132    // `min_indent` is used to know how much whitespaces from the start of each lines must be
133    // removed. Example:
134    //
135    // ///     hello!
136    // #[doc = "another"]
137    //
138    // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
139    // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
140    // (5 - 1) whitespaces.
141    let Some(min_indent) = docs
142        .iter()
143        .map(|fragment| {
144            fragment
145                .doc
146                .as_str()
147                .lines()
148                .filter(|line| line.chars().any(|c| !c.is_whitespace()))
149                .map(|line| {
150                    // Compare against either space or tab, ignoring whether they are
151                    // mixed or not.
152                    let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
153                    whitespace + (if fragment.kind.is_sugared() { 0 } else { add })
154                })
155                .min()
156                .unwrap_or(usize::MAX)
157        })
158        .min()
159    else {
160        return;
161    };
162
163    for fragment in docs {
164        if fragment.doc == sym::empty {
165            continue;
166        }
167
168        let indent = if !fragment.kind.is_sugared() && min_indent > 0 {
169            min_indent - add
170        } else {
171            min_indent
172        };
173
174        fragment.indent = indent;
175    }
176}
177
178/// The goal of this function is to apply the `DocFragment` transformation that is required when
179/// transforming into the final Markdown, which is applying the computed indent to each line in
180/// each doc fragment (a `DocFragment` can contain multiple lines in case of `#[doc = ""]`).
181///
182/// Note: remove the trailing newline where appropriate
183pub fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
184    if frag.doc == sym::empty {
185        out.push('\n');
186        return;
187    }
188    let s = frag.doc.as_str();
189    let mut iter = s.lines();
190
191    while let Some(line) = iter.next() {
192        if line.chars().any(|c| !c.is_whitespace()) {
193            if !(line.len() >= frag.indent) {
    ::core::panicking::panic("assertion failed: line.len() >= frag.indent")
};assert!(line.len() >= frag.indent);
194            out.push_str(&line[frag.indent..]);
195        } else {
196            out.push_str(line);
197        }
198        out.push('\n');
199    }
200}
201
202pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>(
203    attrs: impl Iterator<Item = (&'a A, Option<DefId>)>,
204    doc_only: bool,
205) -> (Vec<DocFragment>, ThinVec<A>) {
206    let (min_size, max_size) = attrs.size_hint();
207    let size_hint = max_size.unwrap_or(min_size);
208    let mut doc_fragments = Vec::with_capacity(size_hint);
209    let mut other_attrs = ThinVec::<A>::with_capacity(if doc_only { 0 } else { size_hint });
210    for (attr, item_id) in attrs {
211        if let Some((doc_str, fragment_kind, attr_span)) = attr.doc_str_and_fragment_kind() {
212            let doc = beautify_doc_string(doc_str, fragment_kind.comment_kind());
213            let (span, from_expansion) = match fragment_kind {
214                DocFragmentKind::Sugared(_) => (attr_span, attr_span.from_expansion()),
215                DocFragmentKind::Raw(value_span) => {
216                    (value_span.with_ctxt(attr_span.ctxt()), value_span.from_expansion())
217                }
218            };
219            let fragment = DocFragment {
220                span,
221                doc,
222                kind: fragment_kind,
223                item_id,
224                indent: 0,
225                from_expansion,
226                style: attr.doc_resolution_scope().unwrap(),
227            };
228            doc_fragments.push(fragment);
229        } else if !doc_only {
230            other_attrs.push(attr.clone());
231        }
232    }
233
234    doc_fragments.shrink_to_fit();
235    other_attrs.shrink_to_fit();
236
237    unindent_doc_fragments(&mut doc_fragments);
238
239    (doc_fragments, other_attrs)
240}
241
242/// Represents a doc comment, split in two parts, outer and inner attributes. It's needed for
243/// the intra-doc link resolution context.
244#[derive(#[automatically_derived]impl ::core::default::Default for DocStrings {
    #[inline]
    fn default() -> Self {
        DocStrings {
            outer: ::core::default::Default::default(),
            inner: ::core::default::Default::default(),
        }
    }
}Default)]
245pub struct DocStrings {
246    pub outer: String,
247    pub inner: String,
248}
249
250/// Return the doc-comments on this item, grouped by the module they came from.
251/// The module can be different if this is a re-export with added documentation.
252///
253/// The last newline is not trimmed so the produced strings are reusable between
254/// early and late doc link resolution regardless of their position.
255pub fn prepare_to_doc_link_resolution(
256    doc_fragments: &[DocFragment],
257) -> FxIndexMap<Option<DefId>, DocStrings> {
258    let mut res: FxIndexMap<Option<DefId>, DocStrings> = FxIndexMap::default();
259    for fragment in doc_fragments {
260        let out_strs = res.entry(fragment.item_id).or_default();
261        let out = if fragment.style == AttrStyle::Inner {
262            &mut out_strs.inner
263        } else {
264            &mut out_strs.outer
265        };
266        add_doc_fragment(out, fragment);
267    }
268    res
269}
270
271/// Options for rendering Markdown in the main body of documentation.
272pub fn main_body_opts() -> Options {
273    Options::ENABLE_TABLES
274        | Options::ENABLE_FOOTNOTES
275        | Options::ENABLE_STRIKETHROUGH
276        | Options::ENABLE_TASKLISTS
277        | Options::ENABLE_SMART_PUNCTUATION
278}
279
280fn strip_generics_from_path_segment(segment: Vec<char>) -> Result<Symbol, MalformedGenerics> {
281    let mut stripped_segment = String::new();
282    let mut param_depth = 0;
283
284    let mut latest_generics_chunk = String::new();
285
286    for c in segment {
287        if c == '<' {
288            param_depth += 1;
289            latest_generics_chunk.clear();
290        } else if c == '>' {
291            param_depth -= 1;
292            if latest_generics_chunk.contains(" as ") {
293                // The segment tries to use fully-qualified syntax, which is currently unsupported.
294                // Give a helpful error message instead of completely ignoring the angle brackets.
295                return Err(MalformedGenerics::HasFullyQualifiedSyntax);
296            }
297        } else if param_depth == 0 {
298            stripped_segment.push(c);
299        } else {
300            latest_generics_chunk.push(c);
301        }
302    }
303
304    if param_depth == 0 {
305        Ok(Symbol::intern(&stripped_segment))
306    } else {
307        // The segment has unbalanced angle brackets, e.g. `Vec<T` or `Vec<T>>`
308        Err(MalformedGenerics::UnbalancedAngleBrackets)
309    }
310}
311
312pub fn strip_generics_from_path(path_str: &str) -> Result<Box<str>, MalformedGenerics> {
313    if !path_str.contains(['<', '>']) {
314        return Ok(path_str.into());
315    }
316    let mut stripped_segments = ::alloc::vec::Vec::new()vec![];
317    let mut path = path_str.chars().peekable();
318    let mut segment = Vec::new();
319
320    while let Some(chr) = path.next() {
321        match chr {
322            ':' => {
323                if path.next_if_eq(&':').is_some() {
324                    let stripped_segment =
325                        strip_generics_from_path_segment(mem::take(&mut segment))?;
326                    if !stripped_segment.is_empty() {
327                        stripped_segments.push(stripped_segment);
328                    }
329                } else {
330                    return Err(MalformedGenerics::InvalidPathSeparator);
331                }
332            }
333            '<' => {
334                segment.push(chr);
335
336                match path.next() {
337                    Some('<') => {
338                        return Err(MalformedGenerics::TooManyAngleBrackets);
339                    }
340                    Some('>') => {
341                        return Err(MalformedGenerics::EmptyAngleBrackets);
342                    }
343                    Some(chr) => {
344                        segment.push(chr);
345
346                        while let Some(chr) = path.next_if(|c| *c != '>') {
347                            segment.push(chr);
348                        }
349                    }
350                    None => break,
351                }
352            }
353            _ => segment.push(chr),
354        }
355        {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event /rustc-dev/9c5648833e5ceb667a708d53c00b83ecc9e610d5/compiler/rustc_resolve/src/rustdoc.rs:355",
                        "rustc_resolve::rustdoc", ::tracing::Level::TRACE,
                        ::tracing_core::__macro_support::Option::Some("/rustc-dev/9c5648833e5ceb667a708d53c00b83ecc9e610d5/compiler/rustc_resolve/src/rustdoc.rs"),
                        ::tracing_core::__macro_support::Option::Some(355u32),
                        ::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};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("raw segment: {0:?}",
                                                    segment) as &dyn ::tracing::field::Value))])
            });
    } else { ; }
};trace!("raw segment: {:?}", segment);
356    }
357
358    if !segment.is_empty() {
359        let stripped_segment = strip_generics_from_path_segment(segment)?;
360        if !stripped_segment.is_empty() {
361            stripped_segments.push(stripped_segment);
362        }
363    }
364
365    {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event /rustc-dev/9c5648833e5ceb667a708d53c00b83ecc9e610d5/compiler/rustc_resolve/src/rustdoc.rs:365",
                        "rustc_resolve::rustdoc", ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("/rustc-dev/9c5648833e5ceb667a708d53c00b83ecc9e610d5/compiler/rustc_resolve/src/rustdoc.rs"),
                        ::tracing_core::__macro_support::Option::Some(365u32),
                        ::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};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("path_str: {0:?}\nstripped segments: {1:?}",
                                                    path_str, stripped_segments) as
                                            &dyn ::tracing::field::Value))])
            });
    } else { ; }
};debug!("path_str: {path_str:?}\nstripped segments: {stripped_segments:?}");
366
367    if !stripped_segments.is_empty() {
368        let stripped_path = join_path_syms(stripped_segments);
369        Ok(stripped_path.into())
370    } else {
371        Err(MalformedGenerics::MissingType)
372    }
373}
374
375/// Has `#[rustc_doc_primitive]` or `#[doc(keyword)]` or `#[doc(attribute)]`.
376pub fn has_primitive_or_keyword_or_attribute_docs_ast(attrs: &[ast::Attribute]) -> bool {
377    for attr in attrs {
378        if attr.has_name(sym::rustc_doc_primitive) {
379            return true;
380        }
381        match &attr.kind {
382            AttrKind::Normal(_) => {
383                if attr.has_name(sym::doc)
384                    && let Some(items) = attr.meta_item_list()
385                {
386                    if items
387                        .iter()
388                        .any(|item| item.has_name(sym::keyword) || item.has_name(sym::attribute))
389                    {
390                        return true;
391                    }
392                }
393            }
394            _ => {}
395        }
396    }
397    false
398}
399
400/// Has `#[rustc_doc_primitive]` or `#[doc(keyword)]` or `#[doc(attribute)]`.
401pub fn has_primitive_or_keyword_or_attribute_docs_hir(attrs: &[Attribute]) -> bool {
402    for attr in attrs {
403        if #[allow(non_exhaustive_omitted_patterns)] match attr {
    Attribute::Parsed(AttributeKind::RustcDocPrimitive(..) |
        AttributeKind::Doc(DocAttribute { attribute: Some(_), .. }) |
        AttributeKind::Doc(DocAttribute { keyword: Some(_), .. })) => true,
    _ => false,
}matches!(
404            attr,
405            Attribute::Parsed(
406                AttributeKind::RustcDocPrimitive(..)
407                    | AttributeKind::Doc(DocAttribute { attribute: Some(_), .. })
408                    | AttributeKind::Doc(DocAttribute { keyword: Some(_), .. })
409            )
410        ) {
411            return true;
412        }
413    }
414    false
415}
416
417/// Simplified version of the corresponding function in rustdoc.
418fn preprocess_link(link: &str) -> Box<str> {
419    // IMPORTANT: To be kept in sync with the corresponding function in rustdoc.
420    // Namely, whenever the rustdoc function returns a successful result for a given input,
421    // this function *MUST* return a link that's equal to `PreprocessingInfo.path_str`!
422
423    let link = link.replace('`', "");
424    let link = link.split('#').next().unwrap();
425    let link = link.trim();
426    let link = link.split_once('@').map_or(link, |(_, rhs)| rhs);
427    let link = link.trim_suffix("()");
428    let link = link.trim_suffix("{}");
429    let link = link.trim_suffix("[]");
430    let link = if link != "!" { link.trim_suffix('!') } else { link };
431    let link = link.trim();
432    strip_generics_from_path(link).unwrap_or_else(|_| link.into())
433}
434
435/// Keep inline and reference links `[]`,
436/// but skip autolinks `<>` which we never consider to be intra-doc links.
437pub fn may_be_doc_link(link_type: LinkType) -> bool {
438    match link_type {
439        LinkType::Inline
440        | LinkType::Reference
441        | LinkType::ReferenceUnknown
442        | LinkType::Collapsed
443        | LinkType::CollapsedUnknown
444        | LinkType::Shortcut
445        | LinkType::ShortcutUnknown => true,
446        LinkType::Autolink | LinkType::Email => false,
447    }
448}
449
450/// Simplified version of `preprocessed_markdown_links` from rustdoc.
451/// Must return at least the same links as it, but may add some more links on top of that.
452pub(crate) fn attrs_to_preprocessed_links(attrs: &[ast::Attribute]) -> Vec<Box<str>> {
453    let (doc_fragments, other_attrs) =
454        attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), false);
455    let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next();
456    let mut links = doc
457        .as_ref()
458        .map(|doc| parse_links(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", doc.outer, doc.inner))
    })format!("{}{}", doc.outer, doc.inner)))
459        .unwrap_or_default();
460
461    for attr in other_attrs {
462        if let Some(note) = attr.deprecation_note() {
463            links.extend(parse_links(note.as_str()));
464        }
465    }
466
467    links
468}
469
470/// Similar version of `markdown_links` from rustdoc.
471/// This will collect destination links and display text if exists.
472fn parse_links<'md>(doc: &'md str) -> Vec<Box<str>> {
473    let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
474    let mut event_iter = Parser::new_with_broken_link_callback(
475        doc,
476        main_body_opts(),
477        Some(&mut broken_link_callback),
478    );
479    let mut links = Vec::new();
480
481    let mut refids = UnordSet::default();
482
483    while let Some(event) = event_iter.next() {
484        match event {
485            Event::Start(Tag::Link { link_type, dest_url, title: _, id })
486                if may_be_doc_link(link_type) =>
487            {
488                if #[allow(non_exhaustive_omitted_patterns)] match link_type {
    LinkType::Inline | LinkType::ReferenceUnknown | LinkType::Reference |
        LinkType::Shortcut | LinkType::ShortcutUnknown => true,
    _ => false,
}matches!(
489                    link_type,
490                    LinkType::Inline
491                        | LinkType::ReferenceUnknown
492                        | LinkType::Reference
493                        | LinkType::Shortcut
494                        | LinkType::ShortcutUnknown
495                ) {
496                    if let Some(display_text) = collect_link_data(&mut event_iter) {
497                        links.push(display_text);
498                    }
499                }
500                if #[allow(non_exhaustive_omitted_patterns)] match link_type {
    LinkType::Reference | LinkType::Shortcut | LinkType::Collapsed => true,
    _ => false,
}matches!(
501                    link_type,
502                    LinkType::Reference | LinkType::Shortcut | LinkType::Collapsed
503                ) {
504                    refids.insert(id);
505                }
506
507                links.push(preprocess_link(&dest_url));
508            }
509            _ => {}
510        }
511    }
512
513    for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
514        if !refids.contains(label) {
515            links.push(preprocess_link(&refdef.dest));
516        }
517    }
518
519    links
520}
521
522/// Collects additional data of link.
523fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
524    event_iter: &mut Parser<'input, F>,
525) -> Option<Box<str>> {
526    let mut display_text: Option<String> = None;
527    let mut append_text = |text: CowStr<'_>| {
528        if let Some(display_text) = &mut display_text {
529            display_text.push_str(&text);
530        } else {
531            display_text = Some(text.to_string());
532        }
533    };
534
535    while let Some(event) = event_iter.next() {
536        match event {
537            Event::Text(text) => {
538                append_text(text);
539            }
540            Event::Code(code) => {
541                append_text(code);
542            }
543            Event::End(_) => {
544                break;
545            }
546            _ => {}
547        }
548    }
549
550    display_text.map(String::into_boxed_str)
551}
552
553/// Returns a span encompassing all the document fragments.
554pub fn span_of_fragments(fragments: &[DocFragment]) -> Option<Span> {
555    let (first_fragment, last_fragment) = match fragments {
556        [] => return None,
557        [first, .., last] => (first, last),
558        [first] => (first, first),
559    };
560    if first_fragment.span == DUMMY_SP {
561        return None;
562    }
563    Some(first_fragment.span.to(last_fragment.span))
564}
565
566/// Attempts to match a range of bytes from parsed markdown to a `Span` in the source code.
567///
568/// This method does not always work, because markdown bytes don't necessarily match source bytes,
569/// like if escapes are used in the string. In this case, it returns `None`.
570///
571/// `markdown` is typically the entire documentation for an item,
572/// after combining fragments.
573///
574/// This method will return `Some` only if one of the following is true:
575///
576/// - The doc is made entirely from sugared doc comments, which cannot contain escapes
577/// - The doc is entirely from a single doc fragment with a string literal exactly equal to
578///   `markdown`.
579/// - The doc comes from `include_str!`
580/// - The doc includes exactly one substring matching `markdown[md_range]` which is contained in a
581///   single doc fragment.
582///
583/// This function is defined in the compiler so it can be used by both `rustdoc` and `clippy`.
584///
585/// It returns a tuple containing a span encompassing all the document fragments and a boolean that
586/// is `true` if any of the *matched* fragments are from a macro expansion.
587pub fn source_span_for_markdown_range(
588    tcx: TyCtxt<'_>,
589    markdown: &str,
590    md_range: &Range<usize>,
591    fragments: &[DocFragment],
592) -> Option<(Span, bool)> {
593    let map = tcx.sess.source_map();
594    source_span_for_markdown_range_inner(map, markdown, md_range, fragments)
595}
596
597// inner function used for unit testing
598pub fn source_span_for_markdown_range_inner(
599    map: &SourceMap,
600    markdown: &str,
601    md_range: &Range<usize>,
602    fragments: &[DocFragment],
603) -> Option<(Span, bool)> {
604    use rustc_span::BytePos;
605
606    if let &[fragment] = &fragments
607        && !fragment.kind.is_sugared()
608        && let Ok(snippet) = map.span_to_snippet(fragment.span)
609        && snippet.trim_end() == markdown.trim_end()
610        && let Ok(md_range_lo) = u32::try_from(md_range.start)
611        && let Ok(md_range_hi) = u32::try_from(md_range.end)
612    {
613        // Single fragment with string that contains same bytes as doc.
614        return Some((
615            Span::new(
616                fragment.span.lo() + rustc_span::BytePos(md_range_lo),
617                fragment.span.lo() + rustc_span::BytePos(md_range_hi),
618                fragment.span.ctxt(),
619                fragment.span.parent(),
620            ),
621            fragment.from_expansion,
622        ));
623    }
624
625    let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind.is_sugared());
626
627    if !is_all_sugared_doc {
628        // This case ignores the markdown outside of the range so that it can
629        // work in cases where the markdown is made from several different
630        // doc fragments, but the target range does not span across multiple
631        // fragments.
632        let mut match_data = None;
633        let pat = &markdown[md_range.clone()];
634        // This heirustic doesn't make sense with a zero-sized range.
635        if pat.is_empty() {
636            return None;
637        }
638        let mut buffer = String::new();
639        let mut offset = 0;
640        for (i, fragment) in fragments.iter().enumerate() {
641            // Ignore fragments outside of the specified range
642            buffer.clear();
643            add_doc_fragment(&mut buffer, fragment);
644            let frag_start = offset;
645            offset += buffer.len();
646            let frag_end = offset;
647            if !(frag_start <= md_range.start && md_range.end <= frag_end) {
648                continue;
649            }
650
651            if let Ok(snippet) = map.span_to_snippet(fragment.span)
652                && let Some(match_start) = snippet.find(pat)
653            {
654                // If there is either a match in a previous fragment, or
655                // multiple matches in this fragment, there is ambiguity.
656                // the snippet cannot be zero-sized, because it matches
657                // the pattern, which is checked to not be zero sized.
658                if match_data.is_none()
659                    && !snippet.as_bytes()[match_start + 1..]
660                        .windows(pat.len())
661                        .any(|s| s == pat.as_bytes())
662                {
663                    match_data = Some((i, match_start));
664                } else {
665                    // Heuristic produced ambiguity, return nothing.
666                    return None;
667                }
668            }
669            break; // only one fragment can contain the range
670        }
671        if let Some((i, match_start)) = match_data {
672            let fragment = &fragments[i];
673            let sp = fragment.span;
674            // we need to calculate the span start,
675            // then use that in our calculations for the span end
676            let lo = sp.lo() + BytePos(match_start as u32);
677            return Some((
678                sp.with_lo(lo).with_hi(lo + BytePos((md_range.end - md_range.start) as u32)),
679                fragment.from_expansion,
680            ));
681        }
682        return None;
683    }
684
685    let snippet = map.span_to_snippet(span_of_fragments(fragments)?).ok()?;
686
687    let starting_line = markdown[..md_range.start].matches('\n').count();
688    let ending_line = starting_line + markdown[md_range.start..md_range.end].matches('\n').count();
689
690    // We use `split_terminator('\n')` instead of `lines()` when counting bytes so that we treat
691    // CRLF and LF line endings the same way.
692    let mut src_lines = snippet.split_terminator('\n');
693    let md_lines = markdown.split_terminator('\n');
694
695    // The number of bytes from the source span to the markdown span that are not part
696    // of the markdown, like comment markers.
697    let mut start_bytes = 0;
698    let mut end_bytes = 0;
699
700    let span_of_all_fragments: Span = span_of_fragments(fragments)?;
701
702    let mut prev_lines_bytes = 0;
703    'outer: for (line_no, md_line) in md_lines.enumerate() {
704        loop {
705            let source_line = src_lines.next()?;
706            let source_line_len = u32::try_from(source_line.len()).unwrap();
707            let source_line_span =
708                span_of_all_fragments.split_at(prev_lines_bytes).1.split_at(source_line_len).0;
709            let fragment = fragments
710                .iter()
711                // `source_line_span` might contain indentation that `fragment.span` doesn't contain
712                .find(|fragment| fragment.span.overlaps(source_line_span));
713            // Since we're counting bytes, `prev_line_bytes` includes the "\n".
714            prev_lines_bytes += source_line_len + 1;
715            if let Some(fragment) = fragment
716                && let Some(offset) = source_line.find(md_line)
717            {
718                if fragment.span.lo() > source_line_span.lo()
719                    && source_line
720                        [..usize::try_from(fragment.span.lo().0 - source_line_span.lo().0).unwrap()]
721                        .chars()
722                        .any(|c| !c.is_whitespace())
723                {
724                    // Make sure anything between the start of this line and the fragment itself is just indentation.
725                    // Because source_line is built by splitting the span that covers all fragments, this only finds
726                    // characters *between* doc comments, not characters before or after doc comments.
727                    //
728                    //     1| /** doc */
729                    //     2| #[inline] /** doc2 */
730                    //        ^^^^^^^^^
731                    //        | this
732                    //
733                    //     3| fn foo() {}
734                    return None;
735                }
736                if fragment.span.hi() < source_line_span.hi()
737                    && source_line
738                        [usize::try_from(fragment.span.hi().0 - source_line_span.lo().0).unwrap()..]
739                        .chars()
740                        .any(|c| !c.is_whitespace())
741                {
742                    // Make sure anything between the start of this line and the fragment itself is just indentation.
743                    // Because source_line is built by splitting the span that covers all fragments, this only finds
744                    // characters *between* doc comments, not characters before or after doc comments.
745                    //     1| /** doc */ #[inline]
746                    //                   ^^^^^^^^^
747                    //                   | this
748                    //
749                    //     2| /** doc2 */
750                    //     3| fn foo() {}
751                    return None;
752                }
753                if line_no == starting_line {
754                    start_bytes += offset;
755
756                    if starting_line == ending_line {
757                        break 'outer;
758                    }
759                } else if line_no == ending_line {
760                    end_bytes += offset;
761                    break 'outer;
762                } else if line_no < starting_line {
763                    start_bytes += source_line.len() - md_line.len();
764                } else {
765                    end_bytes += source_line.len() - md_line.len();
766                }
767                break;
768            } else {
769                // Since this is a source line that doesn't include a markdown line,
770                // we have to count it and its newline as non-markdown bytes.
771                if line_no <= starting_line {
772                    start_bytes += source_line.len() + 1;
773                } else if source_line.chars().any(|c| !c.is_whitespace()) {
774                    // We're past the first line, but haven't found the last line,
775                    // but we found a non-empty non-markdown line.
776                    // This could be an attribute, and we don't want a diagnostic
777                    // suggesting to delete that attribute, so we return None to be safe.
778                    //     1| /** doc */
779                    //     2 | #[inline]
780                    //          ^^^^^^^^^
781                    //          | this
782                    //     3| /** doc2 */
783                    //     4| fn foo() {}
784                    return None;
785                } else {
786                    end_bytes += source_line.len() + 1;
787                }
788            }
789        }
790    }
791
792    let src_span = span_of_all_fragments.from_inner(InnerSpan::new(
793        md_range.start + start_bytes,
794        md_range.end + start_bytes + end_bytes,
795    ));
796    Some((
797        src_span,
798        fragments.iter().any(|frag| frag.span.overlaps(src_span) && frag.from_expansion),
799    ))
800}