Skip to main content

rustdoc/html/
span_map.rs

1use std::path::{Path, PathBuf};
2
3use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
4use rustc_hir as hir;
5use rustc_hir::def::{DefKind, Res};
6use rustc_hir::def_id::{DefId, LOCAL_CRATE};
7use rustc_hir::intravisit::{self, Visitor};
8use rustc_hir::{ExprKind, HirId, Item, ItemKind, Mod, Node, QPath};
9use rustc_middle::hir::nested_filter;
10use rustc_middle::ty::{self, TyCtxt};
11use rustc_span::def_id::LocalModId;
12use rustc_span::{BytePos, ExpnKind};
13
14use crate::clean::{self, PrimitiveType, rustc_span};
15use crate::html::sources;
16
17/// This is a stripped down version of [`rustc_span::Span`] that only contains the start and end
18/// byte positions of the span.
19///
20/// Profiling showed that the `Span` interner was taking up a lot of the run-time when highlighting,
21/// and since we never actually use the context and parent that are stored in a normal `Span`, we
22/// can replace its usages with this one, which is much cheaper to construct.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub(crate) struct Span {
25    lo: BytePos,
26    hi: BytePos,
27}
28
29impl From<rustc_span::Span> for Span {
30    fn from(value: rustc_span::Span) -> Self {
31        Self { lo: value.lo(), hi: value.hi() }
32    }
33}
34
35impl Span {
36    pub(crate) fn lo(self) -> BytePos {
37        self.lo
38    }
39
40    pub(crate) fn hi(self) -> BytePos {
41        self.hi
42    }
43
44    pub(crate) fn with_lo(self, lo: BytePos) -> Self {
45        Self { lo, hi: self.hi() }
46    }
47
48    pub(crate) fn with_hi(self, hi: BytePos) -> Self {
49        Self { lo: self.lo(), hi }
50    }
51}
52
53pub(crate) const DUMMY_SP: Span = Span { lo: BytePos(0), hi: BytePos(0) };
54
55/// This enum allows us to store two different kinds of information:
56///
57/// In case the `span` definition comes from the same crate, we can simply get the `span` and use
58/// it as is.
59///
60/// Otherwise, we store the definition `DefId` and will generate a link to the documentation page
61/// instead of the source code directly.
62#[derive(Debug)]
63pub(crate) enum LinkFromSrc {
64    Local(clean::Span),
65    External(DefId),
66    Primitive(PrimitiveType),
67    Doc(DefId),
68}
69
70/// This function will do at most two things:
71///
72/// 1. Generate a `span` correspondence map which links an item `span` to its definition `span`.
73/// 2. Collect the source code files.
74///
75/// It returns the source code files and the `span` correspondence map.
76///
77/// Note about the `span` correspondence map: the keys are actually `(lo, hi)` of `span`s. We don't
78/// need the `span` context later on, only their position, so instead of keeping a whole `Span`, we
79/// only keep the `lo` and `hi`.
80pub(crate) fn collect_spans_and_sources(
81    tcx: TyCtxt<'_>,
82    krate: &clean::Crate,
83    src_root: &Path,
84    include_sources: bool,
85    generate_link_to_definition: bool,
86) -> (FxIndexMap<PathBuf, String>, FxHashMap<Span, LinkFromSrc>) {
87    if include_sources {
88        let mut visitor =
89            SpanMapVisitor { tcx, maybe_typeck_results: None, matches: FxHashMap::default() };
90
91        if generate_link_to_definition {
92            tcx.hir_walk_toplevel_module(&mut visitor);
93        }
94        let sources = sources::collect_local_sources(tcx, src_root, krate);
95        (sources, visitor.matches)
96    } else {
97        (Default::default(), Default::default())
98    }
99}
100
101struct SpanMapVisitor<'tcx> {
102    pub(crate) tcx: TyCtxt<'tcx>,
103    pub(crate) maybe_typeck_results: Option<LazyTypeckResults<'tcx>>,
104    pub(crate) matches: FxHashMap<Span, LinkFromSrc>,
105}
106
107impl<'tcx> SpanMapVisitor<'tcx> {
108    /// Returns the typeck results of the current body if we're in one.
109    ///
110    /// This will typeck the body if it hasn't been already. Since rustdoc intentionally doesn't run
111    /// all semantic analysis passes on function bodies at the time of writing, this can lead to us
112    /// "suddenly" rejecting the user's code under `--generate-link-to-definition` while accepting
113    /// it if that flag isn't passed! So use this method sparingly and think about the consequences
114    /// including performance!
115    ///
116    /// This behavior is documented in the rustdoc book. Ideally, it wouldn't be that way but no
117    /// good solution has been found so far. Don't think about adding some sort of flag to rustc to
118    /// suppress diagnostic emission that would be unsound wrt. `ErrorGuaranteed`[^1] and generally
119    /// be quite hacky!
120    ///
121    /// [^1]: Historical context:
122    /// <https://github.com/rust-lang/rust/issues/69426#issuecomment-1019412352>.
123    fn maybe_typeck_results(&mut self) -> Option<&'tcx ty::TypeckResults<'tcx>> {
124        let results = self.maybe_typeck_results.as_mut()?;
125        let results = results.cache.get_or_insert_with(|| self.tcx.typeck_body(results.body_id));
126        Some(results)
127    }
128
129    fn link_for_def(&self, def_id: DefId) -> LinkFromSrc {
130        if def_id.is_local() {
131            LinkFromSrc::Local(rustc_span(def_id, self.tcx))
132        } else {
133            LinkFromSrc::External(def_id)
134        }
135    }
136
137    /// This function is where we handle `hir::Path` elements and add them into the "span map".
138    fn handle_path(&mut self, path: &hir::Path<'_>, only_use_last_segment: bool) {
139        match path.res {
140            // FIXME: Properly support type parameters. Note they resolve just fine. The issue is
141            // that our highlighter would then also linkify their *definition site* for some reason
142            // linking them to themselves. Const parameters don't exhibit this issue.
143            Res::Def(DefKind::TyParam, _) => {}
144            Res::Def(_, def_id) => {
145                // The segments can be empty for `use *;` in a non-crate-root scope in Rust 2015.
146                let span = path.segments.last().map_or(path.span, |seg| seg.ident.span);
147                // In case the path ends with generics, we remove them from the span.
148                let span = if only_use_last_segment {
149                    if path.span.from_expansion() {
150                        // For now we don't handle span from macro expansions so nothing to do here.
151                        return;
152                    }
153                    span
154                } else {
155                    // In `use` statements, the included item is not in the path segments. However,
156                    // it doesn't matter because you can't have generics on `use` statements.
157                    if path.span.contains(span) { path.span.with_hi(span.hi()) } else { path.span }
158                };
159                self.matches.insert(span.into(), self.link_for_def(def_id));
160            }
161            Res::Local(_) if let Some(span) = self.tcx.hir_res_span(path.res) => {
162                let path_span = if only_use_last_segment {
163                    path.segments.last().unwrap().ident.span
164                } else {
165                    path.span
166                };
167                self.matches.insert(path_span.into(), LinkFromSrc::Local(clean::Span::new(span)));
168            }
169            Res::PrimTy(p) => {
170                // FIXME: Doesn't handle "path-like" primitives like arrays or tuples.
171                self.matches
172                    .insert(path.span.into(), LinkFromSrc::Primitive(PrimitiveType::from(p)));
173            }
174            _ => {}
175        }
176    }
177
178    /// Used to generate links on items' definition to go to their documentation page.
179    pub(crate) fn extract_info_from_hir_id(&mut self, hir_id: HirId) {
180        if let Node::Item(item) = self.tcx.hir_node(hir_id)
181            && let Some(span) = self.tcx.def_ident_span(item.owner_id)
182        {
183            let cspan = clean::Span::new(span);
184            // If the span isn't from the current crate, we ignore it.
185            if cspan.inner().is_dummy() || cspan.cnum(self.tcx.sess) != LOCAL_CRATE {
186                return;
187            }
188            self.matches.insert(span.into(), LinkFromSrc::Doc(item.owner_id.to_def_id()));
189        }
190    }
191
192    /// Adds the macro call into the span map. Returns `true` if the `span` was inside a macro
193    /// expansion, whether or not it was added to the span map.
194    ///
195    /// The idea for the macro support is to check if the current `Span` comes from expansion. If
196    /// so, we loop until we find the macro definition by using `outer_expn_data` in a loop.
197    /// Finally, we get the information about the macro itself (`span` if "local", `DefId`
198    /// otherwise) and store it inside the span map.
199    fn handle_macro(&mut self, span: rustc_span::Span) -> bool {
200        if !span.from_expansion() {
201            return false;
202        }
203        // So if the `span` comes from a macro expansion, we need to get the original
204        // macro's `DefId`.
205        let mut data = span.ctxt().outer_expn_data();
206        let mut call_site = data.call_site;
207        // Macros can expand to code containing macros, which will in turn be expanded, etc.
208        // So the idea here is to "go up" until we're back to code that was generated from
209        // macro expansion so that we can get the `DefId` of the original macro that was at the
210        // origin of this expansion.
211        while call_site.from_expansion() {
212            data = call_site.ctxt().outer_expn_data();
213            call_site = data.call_site;
214        }
215
216        let macro_name = match data.kind {
217            ExpnKind::Macro(_, macro_name) => macro_name,
218            // Even though we don't handle this kind of macro, this `data` still comes from
219            // expansion so we return `true` so we don't go any deeper in this code.
220            _ => return true,
221        };
222        let link_from_src = match data.macro_def_id {
223            Some(macro_def_id) => {
224                if macro_def_id.is_local() {
225                    LinkFromSrc::Local(clean::Span::new(data.def_site))
226                } else {
227                    LinkFromSrc::External(macro_def_id)
228                }
229            }
230            None => return true,
231        };
232        let new_span = data.call_site;
233        let macro_name = macro_name.as_str();
234        // The "call_site" includes the whole macro with its "arguments". We only want
235        // the macro name.
236        let new_span = new_span.with_hi(new_span.lo() + BytePos(macro_name.len() as u32));
237        self.matches.insert(new_span.into(), link_from_src);
238        true
239    }
240}
241
242impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
243    type NestedFilter = nested_filter::All;
244
245    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
246        self.tcx
247    }
248
249    fn visit_nested_body(&mut self, body_id: hir::BodyId) -> Self::Result {
250        let maybe_typeck_results =
251            self.maybe_typeck_results.replace(LazyTypeckResults { body_id, cache: None });
252        self.visit_body(self.tcx.hir_body(body_id));
253        self.maybe_typeck_results = maybe_typeck_results;
254    }
255
256    fn visit_anon_const(&mut self, ct: &'tcx hir::AnonConst) {
257        // FIXME: Typeck'ing anon consts leads to ICEs in rustc if the parent body wasn't typeck'ed
258        //        yet. See #156418. Figure out what the best and proper solution for this is. Until
259        //        then, let's prevent `typeck` from being called on anon consts by not setting
260        //        `maybe_typeck_results` to `Some(_)`.
261        let maybe_typeck_results = self.maybe_typeck_results.take();
262        self.visit_body(self.tcx.hir_body(ct.body));
263        self.maybe_typeck_results = maybe_typeck_results;
264    }
265
266    fn visit_path(&mut self, path: &hir::Path<'tcx>, _id: HirId) {
267        if self.handle_macro(path.span) {
268            return;
269        }
270        self.handle_path(path, false);
271        intravisit::walk_path(self, path);
272    }
273
274    fn visit_qpath(&mut self, qpath: &QPath<'tcx>, id: HirId, _span: rustc_span::Span) {
275        match *qpath {
276            QPath::TypeRelative(qself, segment) => {
277                // FIXME: This doesn't work for paths in *types* since HIR ty lowering currently
278                //        doesn't write back the resolution of type-relative paths. Updating it to
279                //        do so should be a simple fix.
280                // FIXME: This obviously doesn't support item signatures / non-bodies. Sadly, rustc
281                //        currently doesn't keep around that information & thus can't provide an API
282                //        for it.
283                //        `ItemCtxt`s would need a place to write back the resolution of type-
284                //        dependent definitions. Ideally there was some sort of query keyed on the
285                //        `LocalDefId` of the owning item that returns some table with which we can
286                //        map the `HirId` to a `DefId`.
287                //        Of course, we could re-HIR-ty-lower such paths *here* if we were to extend
288                //        the public API of HIR analysis. However, I strongly advise against it as
289                //        it would be too much of a hack.
290                if let Some(typeck_results) = self.maybe_typeck_results() {
291                    let path = hir::Path {
292                        // We change the span to not include parens.
293                        span: segment.ident.span,
294                        res: typeck_results.qpath_res(qpath, id),
295                        segments: std::slice::from_ref(segment),
296                    };
297                    self.handle_path(&path, false);
298                }
299
300                rustc_ast::visit::try_visit!(self.visit_ty_unambig(qself));
301                self.visit_path_segment(segment);
302            }
303            QPath::Resolved(maybe_qself, path) => {
304                self.handle_path(path, true);
305
306                rustc_ast::visit::visit_opt!(self, visit_ty_unambig, maybe_qself);
307                if !self.handle_macro(path.span) {
308                    intravisit::walk_path(self, path);
309                }
310            }
311        }
312    }
313
314    fn visit_mod(&mut self, m: &'tcx Mod<'tcx>, span: rustc_span::Span, id: LocalModId) {
315        // To make the difference between "mod foo {}" and "mod foo;". In case we "import" another
316        // file, we want to link to it. Otherwise no need to create a link.
317        if !span.overlaps(m.spans.inner_span) {
318            // Now that we confirmed it's a file import, we want to get the span for the module
319            // name only and not all the "mod foo;".
320            if let Node::Item(item) = self.tcx.hir_node_by_def_id(id.into()) {
321                let (ident, _) = item.expect_mod();
322                self.matches.insert(
323                    ident.span.into(),
324                    LinkFromSrc::Local(clean::Span::new(m.spans.inner_span)),
325                );
326            }
327        } else {
328            // If it's a "mod foo {}", we want to look to its documentation page.
329            self.extract_info_from_hir_id(id.into());
330        }
331        intravisit::walk_mod(self, m);
332    }
333
334    fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) {
335        match expr.kind {
336            ExprKind::MethodCall(segment, ..) => {
337                if let Some(typeck_results) = self.maybe_typeck_results()
338                    && let Some(def_id) = typeck_results.type_dependent_def_id(expr.hir_id)
339                {
340                    self.matches.insert(segment.ident.span.into(), self.link_for_def(def_id));
341                }
342            }
343            // We don't want to go deeper into the macro.
344            _ if self.handle_macro(expr.span) => return,
345            _ => {}
346        }
347        intravisit::walk_expr(self, expr);
348    }
349
350    fn visit_item(&mut self, item: &'tcx Item<'tcx>) {
351        // We're no longer in a body since we've crossed an item boundary.
352        // Temporarily take away the typeck results which are only valid in bodies.
353        let maybe_typeck_results = self.maybe_typeck_results.take();
354
355        match item.kind {
356            ItemKind::Static(..)
357            | ItemKind::Const(..)
358            | ItemKind::Fn { .. }
359            | ItemKind::Macro(..)
360            | ItemKind::TyAlias(..)
361            | ItemKind::Enum(..)
362            | ItemKind::Struct(..)
363            | ItemKind::Union(..)
364            | ItemKind::Trait { .. }
365            | ItemKind::TraitAlias(..) => self.extract_info_from_hir_id(item.hir_id()),
366            ItemKind::Impl(_)
367            | ItemKind::Use(..)
368            | ItemKind::ExternCrate(..)
369            | ItemKind::ForeignMod { .. }
370            | ItemKind::GlobalAsm { .. }
371            | ItemKind::TestBinderConstraints { .. }
372            // We already have "visit_mod" above so no need to check it here.
373            | ItemKind::Mod(..) => {}
374        }
375
376        intravisit::walk_item(self, item);
377
378        self.maybe_typeck_results = maybe_typeck_results;
379    }
380}
381
382/// Lazily computed & cached [`ty::TypeckResults`].
383struct LazyTypeckResults<'tcx> {
384    body_id: hir::BodyId,
385    cache: Option<&'tcx ty::TypeckResults<'tcx>>,
386}