Skip to content

Commit a313cd6

Browse files
committed
rustdoc: SpanMapVisitor: Cache TypeckResults
1 parent cb40c25 commit a313cd6

2 files changed

Lines changed: 90 additions & 41 deletions

File tree

src/doc/rustdoc/src/unstable-features.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,15 @@ add the `--scrape-tests` flag.
653653
This flag enables the generation of links in the source code pages which allow the reader
654654
to jump to a type definition.
655655

656+
> [!WARNING]
657+
> In very specific scenarios, enabling this feature may lead to your program getting rejected if you
658+
> rely on rustdoc intentionally not running all semantic analysis passes on function bodies to aid
659+
> with documenting `cfg`-conditional items.
660+
>
661+
> More concretely, rustdoc may choose to type-check bodies if they contain type-dependent paths
662+
> including method calls. This may result in name resolution and type errors getting reported that
663+
> rustdoc would usually suppress.
664+
656665
### `--test-builder`: `rustc`-like program to build tests
657666

658667
* Tracking issue: [#102981](https://github.com/rust-lang/rust/issues/102981)

src/librustdoc/html/render/span_map.rs

Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::path::{Path, PathBuf};
22

33
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
4+
use rustc_hir as hir;
45
use rustc_hir::def::{DefKind, Res};
5-
use rustc_hir::def_id::{DefId, LOCAL_CRATE, LocalDefId};
6+
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
67
use rustc_hir::intravisit::{self, Visitor, VisitorExt};
78
use rustc_hir::{ExprKind, HirId, Item, ItemKind, Mod, Node, QPath};
89
use rustc_middle::hir::nested_filter;
9-
use rustc_middle::ty::TyCtxt;
10+
use rustc_middle::ty::{self, TyCtxt};
1011
use rustc_span::{BytePos, ExpnKind};
1112

1213
use crate::clean::{self, PrimitiveType, rustc_span};
@@ -82,7 +83,8 @@ pub(crate) fn collect_spans_and_sources(
8283
generate_link_to_definition: bool,
8384
) -> (FxIndexMap<PathBuf, String>, FxHashMap<Span, LinkFromSrc>) {
8485
if include_sources {
85-
let mut visitor = SpanMapVisitor { tcx, matches: FxHashMap::default() };
86+
let mut visitor =
87+
SpanMapVisitor { tcx, maybe_typeck_results: None, matches: FxHashMap::default() };
8688

8789
if generate_link_to_definition {
8890
tcx.hir_walk_toplevel_module(&mut visitor);
@@ -96,12 +98,34 @@ pub(crate) fn collect_spans_and_sources(
9698

9799
struct SpanMapVisitor<'tcx> {
98100
pub(crate) tcx: TyCtxt<'tcx>,
101+
pub(crate) maybe_typeck_results: Option<LazyTypeckResults<'tcx>>,
99102
pub(crate) matches: FxHashMap<Span, LinkFromSrc>,
100103
}
101104

102-
impl SpanMapVisitor<'_> {
105+
impl<'tcx> SpanMapVisitor<'tcx> {
106+
/// Returns the typeck results of the current body if we're in one.
107+
///
108+
/// This will typeck the body if it hasn't been already. Since rustdoc intentionally doesn't run
109+
/// all semantic analysis passes on function bodies at the time of writing, this can lead to us
110+
/// "suddenly" rejecting the user's code under `--generate-link-to-definition` while accepting
111+
/// it if that flag isn't passed! So use this method sparingly and think about the consequences
112+
/// including performance!
113+
///
114+
/// This behavior is documented in the rustdoc book. Ideally, it wouldn't be that way but no
115+
/// good solution has been found so far. Don't think about adding some sort of flag to rustc to
116+
/// suppress diagnostic emission that would be unsound wrt. `ErrorGuaranteed`[^1] and generally
117+
/// be quite hacky!
118+
///
119+
/// [^1]: Historical context:
120+
/// <https://github.com/rust-lang/rust/issues/69426#issuecomment-1019412352>.
121+
fn maybe_typeck_results(&mut self) -> Option<&'tcx ty::TypeckResults<'tcx>> {
122+
let results = self.maybe_typeck_results.as_mut()?;
123+
let results = results.cache.get_or_insert_with(|| self.tcx.typeck_body(results.body_id));
124+
Some(*results)
125+
}
126+
103127
/// This function is where we handle `hir::Path` elements and add them into the "span map".
104-
fn handle_path(&mut self, path: &rustc_hir::Path<'_>, only_use_last_segment: bool) {
128+
fn handle_path(&mut self, path: &hir::Path<'_>, only_use_last_segment: bool) {
105129
match path.res {
106130
// FIXME: For now, we handle `DefKind` if it's not a `DefKind::TyParam`.
107131
// Would be nice to support them too alongside the other `DefKind`
@@ -218,18 +242,13 @@ impl SpanMapVisitor<'_> {
218242
}
219243

220244
fn infer_id(&mut self, hir_id: HirId, expr_hir_id: Option<HirId>, span: Span) {
221-
let tcx = self.tcx;
222-
let body_id = tcx.hir_enclosing_body_owner(hir_id);
223-
// FIXME: this is showing error messages for parts of the code that are not
224-
// compiled (because of cfg)!
225-
//
226-
// See discussion in https://github.com/rust-lang/rust/issues/69426#issuecomment-1019412352
227-
let typeck_results = tcx.typeck_body(tcx.hir_body_owned_by(body_id).id());
245+
let Some(typeck_results) = self.maybe_typeck_results() else { return };
246+
228247
// Interestingly enough, for method calls, we need the whole expression whereas for static
229248
// method/function calls, we need the call expression specifically.
230249
if let Some(def_id) = typeck_results.type_dependent_def_id(expr_hir_id.unwrap_or(hir_id)) {
231250
let link = if def_id.as_local().is_some() {
232-
LinkFromSrc::Local(rustc_span(def_id, tcx))
251+
LinkFromSrc::Local(rustc_span(def_id, self.tcx))
233252
} else {
234253
LinkFromSrc::External(def_id)
235254
};
@@ -238,31 +257,21 @@ impl SpanMapVisitor<'_> {
238257
}
239258
}
240259

241-
// This is a reimplementation of `hir_enclosing_body_owner` which allows to fail without
242-
// panicking.
243-
fn hir_enclosing_body_owner(tcx: TyCtxt<'_>, hir_id: HirId) -> Option<LocalDefId> {
244-
for (_, node) in tcx.hir_parent_iter(hir_id) {
245-
// FIXME: associated type impl items don't have an associated body, so we don't handle
246-
// them currently.
247-
if let Node::ImplItem(impl_item) = node
248-
&& matches!(impl_item.kind, rustc_hir::ImplItemKind::Type(_))
249-
{
250-
return None;
251-
} else if let Some((def_id, _)) = node.associated_body() {
252-
return Some(def_id);
253-
}
254-
}
255-
None
256-
}
257-
258260
impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
259261
type NestedFilter = nested_filter::All;
260262

261263
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
262264
self.tcx
263265
}
264266

265-
fn visit_path(&mut self, path: &rustc_hir::Path<'tcx>, _id: HirId) {
267+
fn visit_nested_body(&mut self, body_id: hir::BodyId) -> Self::Result {
268+
let maybe_typeck_results =
269+
self.maybe_typeck_results.replace(LazyTypeckResults { body_id, cache: None });
270+
self.visit_body(self.tcx.hir_body(body_id));
271+
self.maybe_typeck_results = maybe_typeck_results;
272+
}
273+
274+
fn visit_path(&mut self, path: &hir::Path<'tcx>, _id: HirId) {
266275
if self.handle_macro(path.span) {
267276
return;
268277
}
@@ -272,25 +281,37 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
272281

273282
fn visit_qpath(&mut self, qpath: &QPath<'tcx>, id: HirId, _span: rustc_span::Span) {
274283
match *qpath {
275-
QPath::TypeRelative(qself, path) => {
276-
if matches!(path.res, Res::Err) {
277-
let tcx = self.tcx;
278-
if let Some(body_id) = hir_enclosing_body_owner(tcx, id) {
279-
let typeck_results = tcx.typeck_body(tcx.hir_body_owned_by(body_id).id());
280-
let path = rustc_hir::Path {
284+
QPath::TypeRelative(qself, segment) => {
285+
if let Res::Err = segment.res {
286+
// FIXME: This doesn't work for paths in *types* since HIR ty lowering currently
287+
// doesn't write back the resolution of type-relative paths. Updating it
288+
// to do so should be a simple fix.
289+
// FIXME: This obviously doesn't support item signatures / non-bodies. Sadly,
290+
// rustc currently doesn't keep around that information & thus can't
291+
// provide an API for it.
292+
// `ItemCtxt`s would need a place to write back the resolution of type-
293+
// dependent definitions. Ideally there was some sort of query keyed on
294+
// the `LocalDefId` of the owning item that returns some table with which
295+
// we can map the `HirId` to a `DefId`.
296+
// Of course, we could re-HIR-ty-lower such paths *here* if we were to
297+
// extend the public API of HIR analysis. However, I strongly advise
298+
// against it as it would be too much of a hack.
299+
if let Some(typeck_results) = self.maybe_typeck_results() {
300+
let path = hir::Path {
281301
// We change the span to not include parens.
282-
span: path.ident.span,
302+
span: segment.ident.span,
283303
res: typeck_results.qpath_res(qpath, id),
304+
// FIXME(fmease): Don't create a path with zero segments!
284305
segments: &[],
285306
};
286307
self.handle_path(&path, false);
287308
}
288309
} else {
289-
self.infer_id(path.hir_id, Some(id), path.ident.span.into());
310+
self.infer_id(segment.hir_id, Some(id), segment.ident.span.into());
290311
}
291312

292313
rustc_ast::visit::try_visit!(self.visit_ty_unambig(qself));
293-
self.visit_path_segment(path);
314+
self.visit_path_segment(segment);
294315
}
295316
QPath::Resolved(maybe_qself, path) => {
296317
self.handle_path(path, true);
@@ -323,11 +344,17 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
323344
intravisit::walk_mod(self, m);
324345
}
325346

326-
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'tcx>) {
347+
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) {
327348
match expr.kind {
328349
ExprKind::MethodCall(segment, ..) => {
329350
self.infer_id(segment.hir_id, Some(expr.hir_id), segment.ident.span.into())
330351
}
352+
// FIXME(fmease): We needlessly request `TypeckResults` even if the callee isn't
353+
// type-relative. In the majority of cases, it's just gonna be a
354+
// `Resolved` path meaning we can end up unnecessarily
355+
// `typeck`'ing the body which is super costly!
356+
// Moreover, if it actually is a type-relative path, we end up
357+
// "resolving" it twice (with slightly different spans).
331358
ExprKind::Call(call, ..) => self.infer_id(call.hir_id, None, call.span.into()),
332359
_ => {
333360
if self.handle_macro(expr.span) {
@@ -340,6 +367,10 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
340367
}
341368

342369
fn visit_item(&mut self, item: &'tcx Item<'tcx>) {
370+
// We're no longer in a body since we've crossed an item boundary.
371+
// Temporarily take away the typeck results which are only valid in bodies.
372+
let maybe_typeck_results = self.maybe_typeck_results.take();
373+
343374
match item.kind {
344375
ItemKind::Static(..)
345376
| ItemKind::Const(..)
@@ -359,6 +390,15 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
359390
// We already have "visit_mod" above so no need to check it here.
360391
| ItemKind::Mod(..) => {}
361392
}
393+
362394
intravisit::walk_item(self, item);
395+
396+
self.maybe_typeck_results = maybe_typeck_results;
363397
}
364398
}
399+
400+
/// Lazily computed & cached [`ty::TypeckResults`].
401+
struct LazyTypeckResults<'tcx> {
402+
body_id: hir::BodyId,
403+
cache: Option<&'tcx ty::TypeckResults<'tcx>>,
404+
}

0 commit comments

Comments
 (0)