crates/mimir-driver/src/walk.rs
//! The `after_analysis` walk: nodes + edges of ONE crate, from `tcx`.
//!
//! ## Moniker rules (one formatter: `mimir_schema::moniker`)
//!
//! `rust-analyzer cargo <package> <version> <descriptors>` where descriptors are
//! built from the DefId's parent chain:
//!
//! | DefKind | descriptor |
//! |-------------------------------------------|-------------------------------|
//! | crate root | `crate/` (node id only; children start empty) |
//! | Mod | `name/` |
//! | Struct Enum Union Trait TraitAlias TyAlias ForeignTy Variant | `name#` |
//! | Fn (free, nested, foreign) | `name().` |
//! | Const Static Field | `name.` |
//! | Macro (bang) / (derive, attribute) | `name!` / `Name:` |
//! | TyParam ConstParam | `[name]` |
//! | Impl | `impl#[Self]` / `impl#[Self][Trait]` |
//! | AssocFn/Const/Ty in a trait | `Trait#name().` … |
//! | AssocFn/Const/Ty in an impl | `impl#[Self][Trait]name().` (rust-analyzer) |
//! | fn param | `<fn>(name)` (`_<i>` for `_` / unnamed) |
//! | Closure AnonConst | `$n:` (n = rustc's DefPath disambiguator) |
//! | Ctor ForeignMod Use OpaqueTy … | transparent (parent's scope) |
//!
//! Lang crates (std, core, alloc …) carry rust-analyzer's version field
//! `https://github.com/rust-lang/rust/library/<crate>`; the rustc release is in
//! `.stats.json`.
//!
//! The target is byte-identity with `rust-analyzer scip` (tests/fixture_dump.rs
//! `matches_rust_analyzer_scip` diffs the two on the fixture). Deviations exist
//! ONLY where rust-analyzer itself emits duplicate symbols:
//! * a name whose DefPath disambiguator is non-zero (`const _`, hygienic macro
//! twins, serde's `__FieldVisitor`s) gets `$n` appended; anonymous scopes
//! (closures, anon consts) add `$n:`;
//! * an ADT self type with concrete args (`NonZero<u8>`) prints its args;
//! * non-lib targets (bins, integration tests, benches, examples) prefix a
//! meta descriptor `` `<kind>=<crate_name>`: `` (rust-analyzer gives every
//! bin's `main().` and `crate/` the lib's id).
//! * Several inherent impl blocks of one type in one module share one impl node
//! (merged by design, not counted as a collision).
//!
//! Non-lib targets (bins, integration tests, benches, examples) prefix every
//! descriptor with a meta descriptor `` `<kind>=<crate_name>`: ``
//! (`mimir_schema::moniker::target_prefix`), so no two targets of one package
//! share an id — a bin named like the lib, a test and a bench of one name,
//! `main().` of two bins. Which target a unit is comes from cargo metadata
//! (the target whose root file is rustc's input), never from the crate name.
//! A target's `--test` harness build keeps the target's ids (the lib's
//! harness the lib's).
//!
//! ## Visibility and docs (schema 2)
//!
//! Every defined node that has a visibility in rustc carries it twice:
//! `visibility` is the NOMINAL one as rustc resolves it (`tcx.visibility`:
//! `pub` / `pub(crate)` / `pub(in crate::a::b)` / `private`), and
//! `effective_visibility` is how far outside the crate the item really is
//! visible (`tcx.effective_visibilities`: `direct` / `reexported` /
//! `reachable` / `reachable_impl_trait` / `internal`). `doc` is the first line
//! of the doc comment, `has_doc` whether there is one. Reference stubs carry
//! none of it (null): the defining crate's dump has it.
//!
//! ## Attributes (schema 3)
//!
//! Two attributes rustc already has on the item, as nullable booleans:
//! `ignored` — a test fn (`is_test`) carries `#[ignore]`, with or without a
//! reason; null on everything that is not a test. `doc_hidden` — the item
//! ITSELF carries `#[doc(hidden)]` (the items inside a hidden module are not
//! marked, CONTAINS says where they sit); set wherever `has_doc` is, null on
//! reference stubs and on `cfg_gated_out` stubs.
//!
//! ## Tables (schema 4, `tables.rs`)
//!
//! TOUCHES_TABLE: fn → `table` node (id = the table's NAME, one node for the
//! whole log), with `access` (read / write / both) and the sites. Anchored on
//! the storage engine's typed API, derived through every wrapper, written
//! where a table name stops being a parameter; a runtime name is an edge to
//! the one `<unresolved>` table node. What counts, and what does not, is the
//! header of `tables.rs`.
//!
//! ## The FFI border's link name (schema 4)
//!
//! `link_name` — on a foreign fn / static (kind `ffi`), the symbol it is
//! linked as: rustc's symbol name of the item (`#[link_name]` applied), the
//! same name the `.symbols.arrow` row of role `import` carries. Null on every
//! other node and on a reference stub (the declaring crate's dump has it).
//!
//! ## MIR facts (`mir.rs`, `MIMIR_MIR`)
//!
//! Every fn / method body's `mir_promoted` MIR (opt-level independent),
//! closures folded in: a `mir_body` PART per fn (block fingerprints,
//! terminator kinds, successors) for EVERY crate, and for the workspace's own
//! crates a `mir_block` PART per basic block (statements, terminator with the
//! resolved callee, positions relative to the fn, locals with types, local
//! def-use pairs) plus `CFG_SUCC` edges. Payload grammar:
//! `mimir_schema::mir`. No column was added: the lake schema is unchanged.
//!
//! ## Features and gates (`gates.rs`)
//!
//! Feature nodes + ENABLES edges come from the cargo-metadata map; GATED_BY
//! from rustc's own `CfgTrace` on the items it kept; items it compiled OUT are
//! `cfg_gated_out` stubs from the token scan (`mimir_driver::cfg_scan`), and
//! a `cfg` INSIDE an item (a statement, a `use`, `cfg!`) is a `partial`
//! GATED_BY of that item — both for the workspace's own crates
//! (`MIMIR_CFG_SCAN=all` adds the dependencies).
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use mimir_driver::{
artifact_key_of, read_sidecar, stable_crate_id_hex, sysroot_crates, CrateId, CrateStats,
Invocation, VersionMap, ENV_INDEX, ENV_REPO, ENV_SHA, ENV_STD, ENV_VERSIONS,
};
use mimir_schema::moniker::{self, Suffix};
use mimir_schema::{
edge_key, kind, role, visibility, EdgeKind, EdgeRow, EffectiveVisibility, Level, NodeRow, Origin, Provenance,
SpanRow, SymbolRow, SPAN_EDGE, SPAN_NODE,
};
use rustc_ast::attr::data_structures::CfgEntry;
use rustc_hir::def::{DefKind, MacroKinds, Res};
use rustc_hir::def_id::{CrateNum, DefId, LocalDefId, LocalModId, LOCAL_CRATE};
use rustc_hir::intravisit::{self, Visitor};
use rustc_hir::{self as hir};
use rustc_middle::hir::nested_filter;
use rustc_middle::middle::privacy::Level as EffLevel;
use rustc_middle::ty::print::NoTrimmedGuard;
use rustc_middle::ty::{self, GenericArgKind, Instance, Ty, TyCtxt, TypeVisitableExt, TypingEnv};
use rustc_span::hygiene::{AstPass, ExpnKind};
use rustc_span::{FileName, Span};
mod gates;
pub(crate) mod mir;
mod tables;
/// Entry point: walk + write, never panicking out into rustc.
pub fn dump_guarded(tcx: TyCtxt<'_>, inv: &Invocation, out: &Path) {
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let res = std::panic::catch_unwind(AssertUnwindSafe(|| dump(tcx, inv, out)));
std::panic::set_hook(prev_hook);
let err = match res {
Ok(Ok(())) => return,
Ok(Err(e)) => e,
Err(p) => format!(
"panic: {}",
p.downcast_ref::<String>()
.cloned()
.or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default()
),
};
let dir = out.join(".errors");
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(
dir.join(format!("{}-{}.txt", inv.crate_name.as_deref().unwrap_or("?"), inv.short_hash())),
err,
);
}
struct CrateInfo {
package: String,
version: String,
origin: Origin,
}
struct Local {
package: String,
version: String,
origin: Origin,
repo: Option<String>,
sha: Option<String>,
/// descriptor prefix for non-lib targets (`<bin>:`), empty for libs
root_prefix: String,
manifest_dir: Option<PathBuf>,
workspace_root: Option<PathBuf>,
}
struct Walker<'tcx> {
tcx: TyCtxt<'tcx>,
local: Local,
versions: Option<VersionMap>,
index_dir: Option<PathBuf>,
externs: Vec<(String, PathBuf)>,
sysroot: PathBuf,
/// crates written under the lang identity although not loaded from the
/// sysroot (non-empty only in the `-Zbuild-std` dump, see [`ENV_STD`])
lang_crates: BTreeSet<String>,
rustc_release: String,
crate_cache: HashMap<CrateNum, Arc<CrateInfo>>,
desc_cache: HashMap<DefId, Arc<str>>,
nodes: Vec<NodeRow>,
node_ids: HashSet<String>,
stub_done: HashSet<DefId>,
edges: Vec<EdgeRow>,
/// Semantic edge `(from, to, kind)` → its row in `edges`.
edge_index: HashMap<(String, String, EdgeKind), usize>,
/// Distinct sites per edge row (the multiplicity `count`).
edge_sites: HashSet<(usize, Option<String>, Option<u32>)>,
/// Edge sites with a position (`code_spans`, target = edge).
edge_spans: Vec<SpanRow>,
test_fns: HashSet<LocalDefId>,
unresolved: Vec<String>,
version_sources: BTreeMap<String, usize>,
collisions: usize,
collision_samples: Vec<String>,
cross_crate: usize,
cross_repo: usize,
/// features this compile was built with (`--cfg feature="…"`)
active_features: BTreeSet<String>,
/// GATED_BY edge row → (every mention negated, some mention is the
/// item's own `#[cfg]`): what `negated` / `partial` are folded from
gate_state: HashMap<usize, (bool, bool)>,
/// schema-2 counters (visibility / docs / features / gates)
x: Extra,
/// the table pass (TOUCHES_TABLE): summaries, sites, counters
tables: tables::Tables,
/// this unit's sidecar key (`<crate><extra-filename>`)
artifact_key: Option<String>,
}
/// The schema-2 counters of one crate's walk (→ [`CrateStats`]).
#[derive(Default)]
struct Extra {
api_items: usize,
api_documented: usize,
feature_nodes: usize,
feature_refs: usize,
feature_enables_unresolved: Vec<String>,
gated_out_stubs: usize,
cfg_scan_files: usize,
cfg_scan_failed: Vec<String>,
cfg_scan_unresolved_mods: Vec<String>,
feature_cfg_mentions: usize,
feature_cfg_attributed: usize,
feature_cfg_partial: usize,
cfg_scan_ms: u128,
}
/// What gates one item: its own `#[cfg(…)]`s as rustc parsed them.
struct Gate {
/// `cfg(a) && cfg(b)`, canonical spelling.
text: String,
/// each attribute's span with the features it mentions (feature →
/// negated: every mention in that attribute under an odd number of `not`)
sites: Vec<(Span, Vec<(String, bool)>)>,
}
/// Only the table summaries of this crate (its `<key>.tables.json`): the
/// crate is compiled but not dumped (`MIMIR_DUMP=workspace` and it is a
/// dependency), and a crate that IS dumped later calls into it.
pub fn table_summaries_guarded(tcx: TyCtxt<'_>, inv: &Invocation) {
if std::env::var_os(ENV_INDEX).is_none() {
return;
}
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _g1 = NoTrimmedGuard::new();
let (mut w, ..) = walker(tcx, inv);
w.init_tables();
if w.tables.enabled {
w.collect_tests();
w.collect_calls();
w.walk_tables(false);
}
}));
std::panic::set_hook(prev_hook);
if let Err(p) = res {
// not silently "summarises nothing": the dependents must know
let msg = p.downcast_ref::<String>().cloned().or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default();
if let (Some(dir), Some(key)) = (std::env::var_os(ENV_INDEX), inv.artifact_key()) {
let _ = mimir_driver::write_table_sidecar(Path::new(&dir), &key, &mimir_driver::TableSidecar { v: mimir_driver::TABLE_PASS, error: Some(format!("panic: {msg}")), ..Default::default() });
}
}
}
/// The walker of this compile, with the crate's name, package and version.
fn walker<'tcx>(tcx: TyCtxt<'tcx>, inv: &Invocation) -> (Walker<'tcx>, String, String, String) {
let versions = std::env::var_os(ENV_VERSIONS).and_then(|p| VersionMap::load(Path::new(&p)));
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
let package = std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| crate_name.clone());
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "?".into());
let primary = std::env::var_os("CARGO_PRIMARY_PACKAGE").is_some();
// a member by its MANIFEST: one build can hold a member and the published
// copy of that member, under one name and version (`VersionMap::is_member`)
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from);
let member = versions.as_ref().is_some_and(|m| m.is_member(&package, &version, manifest_dir.as_deref()));
let mut origin = if primary || member { Origin::Local } else { Origin::Library };
// The toolchain-crate dump (`-Zbuild-std`): a crate the sysroot ships is
// written under its lang identity — the ids every other dump gives it.
let sysroot = tcx.sess.opts.sysroot.path().to_path_buf();
let lang_crates = if std::env::var(ENV_STD).as_deref() == Ok("1") { sysroot_crates(&sysroot) } else { BTreeSet::new() };
let (package, version) = if lang_crates.contains(&crate_name) {
origin = Origin::Lang;
(crate_name.clone(), moniker::lang_version(&crate_name))
} else {
(package, version)
};
// Which TARGET of its package this unit is decides its ids
// (`moniker::target_prefix`): the lib — and the lib's `--test` harness —
// has rust-analyzer's, any other target is prefixed with kind + name. The
// target is the one whose root file is rustc's input (cargo metadata); a
// bin, a test and a bench may all carry the lib's crate NAME.
let target_kind = match (&versions, &inv.input) {
(Some(m), Some(input)) => m.target_kind(&package, &version, input).map(str::to_string),
_ => None,
};
let root_prefix = match target_kind {
Some(kind) => moniker::target_prefix(&kind, &crate_name),
// no declared target (a dependency — only its lib is compiled — or a
// run without cargo metadata): by what the argv says
None if inv.is_lib() => String::new(),
None => moniker::target_prefix(if inv.is_test { "test" } else { "bin" }, &crate_name),
};
let repo = if origin == Origin::Local {
std::env::var(ENV_REPO).ok().or_else(|| versions.as_ref().map(|m| m.repo.clone()))
} else {
None
};
let workspace_root = versions.as_ref().map(|m| PathBuf::from(&m.workspace_root));
let rustc_release = rustc_interface::util::rustc_version_str()
.and_then(|s| s.split_whitespace().next())
.unwrap_or("?")
.to_string();
let w = Walker {
tcx,
local: Local {
package: package.clone(),
version: version.clone(),
origin,
repo,
sha: std::env::var(ENV_SHA).ok(),
root_prefix,
manifest_dir: std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from),
workspace_root,
},
versions,
index_dir: std::env::var_os(ENV_INDEX).map(PathBuf::from),
externs: inv.externs.clone(),
sysroot,
lang_crates,
rustc_release,
crate_cache: HashMap::new(),
desc_cache: HashMap::new(),
nodes: Vec::new(),
node_ids: HashSet::new(),
stub_done: HashSet::new(),
edges: Vec::new(),
edge_index: HashMap::new(),
edge_sites: HashSet::new(),
edge_spans: Vec::new(),
test_fns: HashSet::new(),
unresolved: Vec::new(),
version_sources: BTreeMap::new(),
collisions: 0,
collision_samples: Vec::new(),
cross_crate: 0,
cross_repo: 0,
active_features: BTreeSet::new(),
gate_state: HashMap::new(),
x: Extra::default(),
tables: tables::Tables::default(),
artifact_key: inv.artifact_key(),
};
(w, crate_name, package, version)
}
fn dump(tcx: TyCtxt<'_>, inv: &Invocation, out: &Path) -> Result<(), String> {
let t0 = Instant::now();
// NEVER touch the trimmed_def_paths query (it ICEs when no diagnostic was
// emitted): types in monikers print with full (visible) paths; the human
// `ty` column prints full paths cut to their last segment (see `plain_ty`).
let _g1 = NoTrimmedGuard::new();
let (mut w, crate_name, package, version) = walker(tcx, inv);
w.init_tables_guarded();
w.collect_tests();
w.walk_features();
w.walk_defs();
w.walk_crate_deps();
w.walk_bodies();
w.walk_tables_guarded();
w.walk_gated_out();
// MIR facts last: they only ADD rows (parts under fns the walk wrote)
let mir_mode = mir::Mode::of_env(w.local.origin == Origin::Local);
// a panic in it loses this crate's MIR facts only (`mir_error`)
let (n0, e0) = (w.nodes.len(), w.edges.len());
let mut mir_error = None;
let mir_st = match std::panic::catch_unwind(AssertUnwindSafe(|| w.walk_mir(mir_mode))) {
Ok(s) => s,
Err(p) => {
mir::clear_stash();
for n in w.nodes.drain(n0..) {
w.node_ids.remove(&n.id);
}
for e in w.edges.drain(e0..) {
w.edge_index.remove(&(e.from, e.to, e.kind));
}
w.edge_sites.retain(|(i, _, _)| *i < e0);
mir_error = Some(format!("panic: {}", p.downcast_ref::<String>().cloned().or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default()));
mir::MirStats { mode: "off", ..Default::default() }
}
};
let walk_ms = t0.elapsed().as_millis();
let ts = Instant::now();
let (symbols, symbol_errors) = w.walk_symbols();
let crate_ids = w.crate_ids();
let symbols_ms = ts.elapsed().as_millis();
// ---- write ----
let t1 = Instant::now();
std::fs::create_dir_all(out).map_err(|e| e.to_string())?;
// a lang crate's version is a URL: its files say `@lang`
let file_version = if w.local.origin == Origin::Lang { "lang".to_string() } else { version.replace(['/', ':'], "_") };
let stem = format!("{crate_name}@{file_version}-{}", inv.short_hash());
let nodes_file = format!("{stem}.nodes.arrow");
let edges_file = format!("{stem}.edges.arrow");
let spans_file = format!("{stem}.spans.arrow");
// Positions live apart from identity: each defined node's span + every
// positioned edge site.
let mut spans: Vec<SpanRow> = w
.nodes
.iter()
.filter(|n| n.defined_here)
.filter_map(|n| {
let (file, start) = (n.file.clone()?, n.start_line?);
Some(SpanRow { target: SPAN_NODE, key: n.id.clone(), file, start_line: start, end_line: n.end_line.unwrap_or(start) })
})
.collect();
spans.append(&mut w.edge_spans);
spans.sort();
spans.dedup();
let nb = mimir_schema::nodes_batch(&w.nodes).map_err(|e| e.to_string())?;
let eb = mimir_schema::edges_batch(&w.edges).map_err(|e| e.to_string())?;
let sb = mimir_schema::spans_batch(&spans).map_err(|e| e.to_string())?;
write_ipc(&out.join(&nodes_file), &nb)?;
write_ipc(&out.join(&edges_file), &eb)?;
write_ipc(&out.join(&spans_file), &sb)?;
let symbols_file = format!("{stem}.symbols.arrow");
let yb = mimir_schema::symbols_batch(&symbols).map_err(|e| e.to_string())?;
write_ipc(&out.join(&symbols_file), &yb)?;
let write_ms = t1.elapsed().as_millis();
let mut by_kind = BTreeMap::new();
for e in &w.edges {
*by_kind.entry(e.kind.as_str().to_string()).or_insert(0) += 1;
}
let mut by_vis: BTreeMap<String, usize> = BTreeMap::new();
let mut by_eff: BTreeMap<String, usize> = BTreeMap::new();
for n in w.nodes.iter().filter(|n| n.defined_here && n.kind != kind::CFG_GATED_OUT) {
if let Some(v) = &n.visibility {
let key = if v.starts_with("pub(in ") { "pub(in …)" } else { v.as_str() };
*by_vis.entry(key.to_string()).or_insert(0) += 1;
}
if let Some(e) = n.effective_visibility {
*by_eff.entry(e.as_str().to_string()).or_insert(0) += 1;
}
}
let documented_nodes = w.nodes.iter().filter(|n| n.defined_here && n.kind != kind::CFG_GATED_OUT && n.has_doc == Some(true)).count();
// GATED_BY by what it says: a kept item's own gate / a compiled-out
// stub's own gate / a gate on something inside an item
let stub_ids: HashSet<&str> = w.nodes.iter().filter(|n| n.kind == kind::CFG_GATED_OUT).map(|n| n.id.as_str()).collect();
let (mut gated_by_active, mut gated_by_stubs, mut gated_by_partial) = (0, 0, 0);
for e in w.edges.iter().filter(|e| e.kind == EdgeKind::GatedBy) {
match (e.partial == Some(true), stub_ids.contains(e.from.as_str())) {
(true, _) => gated_by_partial += 1,
(false, true) => gated_by_stubs += 1,
(false, false) => gated_by_active += 1,
}
}
let stats = CrateStats {
crate_name,
package,
version,
hash: inv.short_hash(),
origin: w.local.origin.as_str().into(),
crate_types: inv.crate_types.clone(),
test_harness: inv.is_test,
nodes: w.nodes.len(),
nodes_defined: w.nodes.iter().filter(|n| n.defined_here).count(),
edges: w.edges.len(),
edge_sites: w.edges.iter().map(|e| e.count as usize).sum(),
spans: spans.len(),
edges_by_kind: by_kind,
cross_crate_edges: w.cross_crate,
cross_repo_edges: w.cross_repo,
unresolved_versions: w.unresolved.clone(),
version_sources: w.version_sources.clone(),
rustc_release: w.rustc_release.clone(),
moniker_collisions: w.collisions,
collision_samples: w.collision_samples.clone(),
walk_ms,
write_ms,
nodes_file,
edges_file,
spans_file,
semhash: mimir_driver::unit_semhash_of_env(&inv),
stable_crate_id: stable_crate_id_hex(tcx.stable_crate_id(LOCAL_CRATE).as_u64()),
crate_ids,
symbol_mangling: format!("{:?}", tcx.sess.opts.get_symbol_mangling_version()).to_lowercase(),
symbols: symbols.len(),
symbols_file,
symbol_errors,
symbols_ms,
nodes_by_visibility: by_vis,
nodes_by_effective_visibility: by_eff,
api_items: w.x.api_items,
api_documented: w.x.api_documented,
documented_nodes,
feature_nodes: w.x.feature_nodes,
features_active: w.active_features.len(),
feature_refs: w.x.feature_refs,
feature_enables_unresolved: std::mem::take(&mut w.x.feature_enables_unresolved),
gated_by_active,
gated_out_stubs: w.x.gated_out_stubs,
gated_by_stubs,
gated_by_partial,
cfg_scan_files: w.x.cfg_scan_files,
cfg_scan_failed: std::mem::take(&mut w.x.cfg_scan_failed),
cfg_scan_unresolved_mods: std::mem::take(&mut w.x.cfg_scan_unresolved_mods),
feature_cfg_mentions: w.x.feature_cfg_mentions,
feature_cfg_attributed: w.x.feature_cfg_attributed,
feature_cfg_partial: w.x.feature_cfg_partial,
cfg_scan_ms: w.x.cfg_scan_ms,
table_pass: w.tables.enabled,
table_pass_error: w.tables.error.take(),
table_sites: w.tables.n_sites,
table_sites_unresolved: w.tables.n_unresolved,
tables: w.tables.names.iter().cloned().collect(),
table_summaries: w.tables.n_summaries,
table_bodies: w.tables.bodies,
table_body_visits: w.tables.visits,
table_sidecars: w.tables.sidecars,
table_sidecars_stale: std::mem::take(&mut w.tables.stale),
table_annotations: w.tables.annotations,
table_annotations_bad: std::mem::take(&mut w.tables.bad_annotations),
tables_ms: w.tables.ms,
mir_facts: if mir_st.mode == "off" { 0 } else { mimir_driver::MIR_FACTS },
mir_error,
mir_mode: mir_st.mode.to_string(),
mir_bodies: mir_st.bodies,
mir_nested_bodies: mir_st.nested,
mir_blocks: mir_st.blocks,
mir_statements: mir_st.statements,
mir_missing: mir_st.missing,
mir_payload_bytes: mir_st.payload_bytes,
mir_ms: mir_st.ms,
};
let tmp = out.join(format!("{stem}.stats.json.tmp"));
std::fs::write(&tmp, serde_json::to_vec_pretty(&stats).map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
std::fs::rename(&tmp, out.join(format!("{stem}.stats.json"))).map_err(|e| e.to_string())?;
Ok(())
}
fn write_ipc(path: &Path, batch: &arrow_array::RecordBatch) -> Result<(), String> {
let tmp = path.with_extension("arrow.tmp");
{
let f = std::fs::File::create(&tmp).map_err(|e| e.to_string())?;
let mut w = arrow_ipc::writer::FileWriter::try_new(std::io::BufWriter::new(f), &batch.schema())
.map_err(|e| e.to_string())?;
w.write(batch).map_err(|e| e.to_string())?;
w.finish().map_err(|e| e.to_string())?;
}
std::fs::rename(&tmp, path).map_err(|e| e.to_string())
}
impl<'tcx> Walker<'tcx> {
// ------------------------------------------------------------------ crates
fn crate_info(&mut self, cnum: CrateNum) -> Arc<CrateInfo> {
if let Some(c) = self.crate_cache.get(&cnum) {
return c.clone();
}
let info = Arc::new(self.compute_crate_info(cnum));
self.crate_cache.insert(cnum, info.clone());
info
}
fn compute_crate_info(&mut self, cnum: CrateNum) -> CrateInfo {
if cnum == LOCAL_CRATE {
return CrateInfo {
package: self.local.package.clone(),
version: self.local.version.clone(),
origin: self.local.origin,
};
}
let name = self.tcx.crate_name(cnum).to_string();
let src = self.tcx.used_crate_source(cnum);
let paths: Vec<PathBuf> = src.paths().cloned().collect();
if paths.iter().any(|p| p.starts_with(&self.sysroot)) || self.lang_crates.contains(&name) {
*self.version_sources.entry("lang".into()).or_default() += 1;
// rust-analyzer's version field for the toolchain's crates
let version = moniker::lang_version(&name);
return CrateInfo { package: name, version, origin: Origin::Lang };
}
if let Some(dir) = &self.index_dir {
for p in &paths {
if let Some(sc) = artifact_key_of(p).and_then(|k| read_sidecar(dir, &k)) {
*self.version_sources.entry("sidecar".into()).or_default() += 1;
// what the unit itself said when it was compiled; a sidecar
// from before it said so: by name and version
// a member of THIS build, whichever build compiled it
// (a shared target dir hands one artifact to many)
let local = sc.local_in(self.versions.as_ref());
return CrateInfo {
package: sc.package,
version: sc.version,
origin: if local { Origin::Local } else { Origin::Library },
};
}
}
}
// direct dep under an ambiguous name: --extern NAME=PATH ↔ loaded file
if let Some(m) = &self.versions {
let key = format!("{} {}", self.local.package, self.local.version);
if let Some(deps) = m.extern_deps.get(&key) {
for (ext, p) in &self.externs {
let canon = |x: &Path| std::fs::canonicalize(x).unwrap_or_else(|_| x.to_path_buf());
if paths.iter().any(|x| x == p || canon(x) == canon(p)) {
if let Some((pn, pv)) = deps.get(ext) {
*self.version_sources.entry("extern".into()).or_default() += 1;
let local = m.packages.iter().any(|x| x.local && &x.name == pn && &x.version == pv);
return CrateInfo {
package: pn.clone(),
version: pv.clone(),
origin: if local { Origin::Local } else { Origin::Library },
};
}
}
}
}
}
if let Some(m) = &self.versions {
let hits = m.by_crate(&name);
if hits.len() == 1 {
*self.version_sources.entry("map".into()).or_default() += 1;
let p = hits[0];
return CrateInfo {
package: p.name.clone(),
version: p.version.clone(),
origin: if p.local { Origin::Local } else { Origin::Library },
};
}
}
*self.version_sources.entry("unresolved".into()).or_default() += 1;
self.unresolved.push(name.clone());
CrateInfo { package: name, version: "?".into(), origin: Origin::Library }
}
// --------------------------------------------------------------- monikers
fn name_of(&self, did: DefId) -> String {
self.tcx.opt_item_name(did).map(|s| s.to_string()).unwrap_or_else(|| "_".into())
}
/// The name as it goes into a DESCRIPTOR: rustc's DefPath disambiguator is
/// non-zero exactly when two defs share (parent, namespace, name) — `const
/// _`, hygienic macro-generated twins, synthetic `impl Trait` params — and
/// then it is appended as `$n` (`$` cannot occur in a Rust ident, so no
/// clash with a real name). Unique names are untouched (coderoom-equal).
fn desc_name(&self, did: DefId) -> String {
let name = self.name_of(did);
let dis = self.tcx.def_key(did).disambiguated_data.disambiguator;
// a `#[test]` fn shares its name with the harness's marker static
if dis == 0 || did.as_local().is_some_and(|l| self.test_fns.contains(&l)) {
name
} else {
format!("{name}${dis}")
}
}
/// Descriptor chain of `did` (scope + own descriptor), cached.
fn desc(&mut self, did: DefId) -> Arc<str> {
if let Some(d) = self.desc_cache.get(&did) {
return d.clone();
}
let d: Arc<str> = self.compute_desc(did).into();
self.desc_cache.insert(did, d.clone());
d
}
fn compute_desc(&mut self, did: DefId) -> String {
if did.is_crate_root() {
return if did.is_local() { self.local.root_prefix.clone() } else { String::new() };
}
let tcx = self.tcx;
let parent = tcx.parent(did);
let mut s = String::new();
let dk = tcx.def_kind(did);
let name = self.desc_name(did);
match dk {
DefKind::Mod => {
s.push_str(&self.desc(parent));
moniker::push_descriptor(&mut s, Suffix::Namespace, &name);
}
DefKind::Struct
| DefKind::Enum
| DefKind::Union
| DefKind::Trait
| DefKind::TraitAlias
| DefKind::TyAlias
| DefKind::ForeignTy
| DefKind::Variant => {
s.push_str(&self.desc(parent));
moniker::push_descriptor(&mut s, Suffix::Type, &name);
}
DefKind::Fn => {
s.push_str(&self.desc(parent));
moniker::push_descriptor(&mut s, Suffix::Method, &name);
}
DefKind::Const { .. } | DefKind::Static { .. } | DefKind::Field => {
s.push_str(&self.desc(parent));
moniker::push_descriptor(&mut s, Suffix::Term, &name);
}
DefKind::Macro(kinds) => {
s.push_str(&self.desc(parent));
// rust-analyzer: bang macros `name!`, derive / attribute macros `Name:`
let suf = if kinds.contains(MacroKinds::BANG) { Suffix::Macro } else { Suffix::Meta };
moniker::push_descriptor(&mut s, suf, &name);
}
DefKind::TyParam | DefKind::ConstParam | DefKind::LifetimeParam => {
s.push_str(&self.desc(parent));
moniker::push_descriptor(&mut s, Suffix::TypeParameter, &name);
}
DefKind::AssocFn | DefKind::AssocConst { .. } | DefKind::AssocTy => {
// trait: `Trait#m().`; impl: rust-analyzer's `impl#[Self][Trait]m().`
s.push_str(&self.desc(parent));
let suf = match dk {
DefKind::AssocFn => Suffix::Method,
DefKind::AssocTy => Suffix::Type,
_ => Suffix::Term,
};
moniker::push_descriptor(&mut s, suf, &name);
}
DefKind::Impl { .. } => {
s.push_str(&self.desc(parent));
let (self_name, trait_name) = self.impl_names(did);
moniker::push_impl(&mut s, &self_name, trait_name.as_deref());
}
// transparent: Ctor, ForeignMod, Use, ExternCrate, GlobalAsm,
// AnonConst, InlineConst, Closure, OpaqueTy, SyntheticCoroutineBody …
// — except anonymous scopes (closures, anon consts), which add a
// `$n:` meta descriptor (n = rustc's disambiguator), so items in
// sibling scopes (tracing's `__CALLSITE`, thread_local!'s
// `__RUST_STD_INTERNAL_VAL`) stay apart.
_ => {
s.push_str(&self.desc(parent));
let dis = tcx.def_key(did).disambiguated_data.disambiguator;
if matches!(dk, DefKind::AnonConst | DefKind::Closure) {
moniker::push_descriptor(&mut s, Suffix::Meta, &format!("${dis}"));
}
}
}
s
}
/// `(Self, Some(Trait))` names for rust-analyzer's `impl#[Self][Trait]`.
///
/// rust-analyzer's spelling: an ADT self type is its bare name (`String`),
/// any other self type prints (`i32`, `T`); the trait is its bare name, with
/// its args when it has any beyond Self, the self type written `Self`
/// (``[`Add<Self>`]``, ``[`Into<U>`]``). One deviation, where rust-analyzer
/// itself emits duplicate symbols: an ADT instantiated with concrete args
/// (`impl X for NonZero<u8>` / `NonZero<u16>` …, macro-generated families)
/// prints with its args.
fn impl_names(&mut self, impl_id: DefId) -> (String, Option<String>) {
let tcx = self.tcx;
let self_ty = tcx.type_of(impl_id).instantiate_identity().skip_normalization();
let self_name = match self_ty.kind() {
ty::Adt(adt, args) if args.iter().all(is_param_arg) => self.desc_name(adt.did()),
ty::Foreign(d) => self.desc_name(*d),
_ => plain_ty(self_ty),
};
let trait_name = if tcx.impl_is_of_trait(impl_id) {
let tr = tcx.impl_trait_ref(impl_id).instantiate_identity().skip_normalization();
let base = tcx.item_name(tr.def_id).to_string();
let args: Vec<String> = tr
.args
.iter()
.skip(1)
.filter_map(|a| match a.kind() {
GenericArgKind::Type(t) if t == self_ty => Some("Self".to_string()),
GenericArgKind::Type(t) => Some(plain_ty(t)),
GenericArgKind::Const(c) => Some({
let _g = NoTrimmedGuard::new();
mimir_driver::shorten_paths(&c.to_string())
}),
GenericArgKind::Lifetime(r) => Some(r.to_string()),
})
.collect();
Some(if args.is_empty() { base } else { format!("{base}<{}>", args.join(", ")) })
} else {
None
};
(self_name, trait_name)
}
fn moniker(&mut self, did: DefId) -> String {
let ci = self.crate_info(did.krate);
if did.is_crate_root() {
let d = if did.is_local() {
format!("{}{}", self.local.root_prefix, moniker::CRATE_ROOT_DESCRIPTOR)
} else {
moniker::CRATE_ROOT_DESCRIPTOR.to_string()
};
return moniker::moniker(&ci.package, &ci.version, &d);
}
let d = self.desc(did);
moniker::moniker(&ci.package, &ci.version, &d)
}
// ------------------------------------------------------------ node kinds
fn node_kind(&self, did: DefId) -> Option<(&'static str, Level)> {
let tcx = self.tcx;
if did.is_crate_root() {
return Some((kind::CRATE, Level::Crate));
}
let dk = tcx.def_kind(did);
Some(match dk {
DefKind::Mod => (kind::MOD, Level::Module),
DefKind::Struct => (kind::STRUCT, Level::Item),
DefKind::Enum => (kind::ENUM, Level::Item),
DefKind::Union => (kind::UNION, Level::Item),
DefKind::Trait => (kind::TRAIT, Level::Item),
DefKind::TraitAlias => (kind::TRAIT_ALIAS, Level::Item),
DefKind::TyAlias => (kind::TYPE, Level::Item),
DefKind::ForeignTy => (kind::FOREIGN_TYPE, Level::Item),
DefKind::Fn => {
if tcx.is_foreign_item(did) {
(kind::FFI, Level::Item)
} else {
(kind::FN, Level::Item)
}
}
DefKind::Const { .. } => (kind::CONST, Level::Item),
DefKind::Static { nested: false, .. } => {
if tcx.is_foreign_item(did) {
(kind::FFI, Level::Item)
} else {
(kind::STATIC, Level::Item)
}
}
DefKind::Macro(_) => (kind::MACRO, Level::Item),
DefKind::Impl { .. } => (kind::IMPL, Level::Item),
DefKind::AssocFn => (kind::METHOD, Level::Part),
DefKind::AssocConst { .. } => (kind::ASSOC_CONST, Level::Part),
DefKind::AssocTy => {
if tcx.is_impl_trait_in_trait(did) {
return None;
}
(kind::ASSOC_TYPE, Level::Part)
}
DefKind::Field => (kind::FIELD, Level::Part),
DefKind::Variant => (kind::VARIANT, Level::Part),
DefKind::TyParam | DefKind::ConstParam => (kind::GENERIC, Level::Part),
_ => return None,
})
}
fn node_parent(&self, did: DefId) -> Option<DefId> {
let mut p = self.tcx.opt_parent(did);
while let Some(x) = p {
if self.node_kind(x).is_some() {
return Some(x);
}
p = self.tcx.opt_parent(x);
}
None
}
/// Nearest node (self or ancestor) — the `from` of edges out of bodies.
fn node_or_ancestor(&self, did: DefId) -> Option<DefId> {
if self.node_kind(did).is_some() { Some(did) } else { self.node_parent(did) }
}
// ------------------------------------------------------------ symbols
/// rustc's symbol name for every LOCAL item that is codegen'd as itself:
/// non-generic fns / methods (impl not generic either), statics, and
/// foreign fns / statics (their link name — the FFI border). Generic items
/// have no symbol of their own (their instances are the binary layer's
/// `mono_instance` nodes). Each computation runs under `catch_unwind`; a
/// panic skips that item and is counted, never fakes a row.
fn walk_symbols(&mut self) -> (Vec<SymbolRow>, usize) {
let tcx = self.tcx;
let mut rows = Vec::new();
let mut errors = 0usize;
let ids: Vec<LocalDefId> = tcx.iter_local_def_id().collect();
for did in ids {
let d = did.to_def_id();
let dk = tcx.def_kind(d);
if !matches!(dk, DefKind::Fn | DefKind::AssocFn | DefKind::Static { nested: false, .. }) {
continue;
}
if self.is_test_marker(did) || self.is_harness_item(did) {
continue;
}
// trait methods (declared or provided) are generic over Self
if dk == DefKind::AssocFn && tcx.trait_of_assoc(d).is_some() {
continue;
}
if tcx.generics_of(d).requires_monomorphization(tcx) {
continue;
}
let id = self.moniker(d);
if !self.node_ids.contains(&id) {
continue;
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
tcx.symbol_name(Instance::mono(tcx, d)).name.to_string()
}));
let Ok(sym) = res else {
errors += 1;
continue;
};
rows.push(SymbolRow {
id,
symbol: sym,
demangled: None,
role: if tcx.is_foreign_item(d) { role::IMPORT.into() } else { role::ITEM.into() },
krate: self.local.package.clone(),
version: self.local.version.clone(),
binary: None,
section: None,
addr: None,
size: None,
provenance: Provenance::SourceOnly,
valid_from_sha: self.local.sha.clone(),
valid_to_sha: None,
});
}
(rows, errors)
}
/// The local crate + every crate this compile loaded, with the
/// StableCrateId rustc mangles into their symbols.
fn crate_ids(&mut self) -> Vec<CrateId> {
let tcx = self.tcx;
let mut out = Vec::new();
let mut cnums = vec![LOCAL_CRATE];
cnums.extend(tcx.crates(()).iter().copied());
for cnum in cnums {
let ci = self.crate_info(cnum);
out.push(CrateId {
crate_name: tcx.crate_name(cnum).to_string(),
stable_crate_id: stable_crate_id_hex(tcx.stable_crate_id(cnum).as_u64()),
package: ci.package.clone(),
version: ci.version.clone(),
origin: ci.origin.as_str().to_string(),
prefix: if cnum == LOCAL_CRATE { self.local.root_prefix.clone() } else { String::new() },
});
}
out
}
// ---------------------------------------------------------------- spans
fn file_lines(&self, span: Span, local: bool) -> (Option<String>, Option<u32>, Option<u32>) {
if span.is_dummy() {
return (None, None, None);
}
let span = span.source_callsite();
let sm = self.tcx.sess.source_map();
let lo = sm.lookup_char_pos(span.lo());
let hi = sm.lookup_char_pos(span.hi());
let file = match &lo.file.name {
FileName::Real(_) if local => Some(self.rel_path(&lo.file.name.prefer_local_unconditionally().to_string())),
FileName::Real(_) => Some(lo.file.name.prefer_remapped_unconditionally().to_string()),
other => Some(other.prefer_remapped_unconditionally().to_string()),
};
(file, Some(lo.line as u32), Some(hi.line as u32))
}
fn rel_path(&self, p: &str) -> String {
let path = Path::new(p);
if let Some(g) = mimir_driver::out_dir_relative(path, std::env::var_os("OUT_DIR").as_deref().map(Path::new)) {
return g;
}
if path.is_absolute() {
if self.local.origin == Origin::Local {
if let Some(r) = self.local.workspace_root.as_ref().and_then(|w| path.strip_prefix(w).ok()) {
return r.to_string_lossy().into_owned();
}
}
if let Some(r) = self.local.manifest_dir.as_ref().and_then(|m| path.strip_prefix(m).ok()) {
return r.to_string_lossy().into_owned();
}
}
p.to_string()
}
/// The item's own `#[cfg(…)]`s — the ones rustc evaluated TRUE and kept as
/// `CfgTrace` (an item whose cfg is false is not here at all). A foreign
/// item also carries the cfg of its `extern` block (the block is not a
/// node, so the gate would otherwise sit on nothing).
fn cfg_of(&self, did: LocalDefId) -> Option<Gate> {
let tcx = self.tcx;
let mut traces: Vec<&(CfgEntry, Span)> = Vec::new();
if let Some(parent) = tcx.opt_local_parent(did) {
if tcx.def_kind(parent) == DefKind::ForeignMod {
if let Some(v) = rustc_attr_ir::find_attr!(tcx, parent, CfgTrace(v) => v) {
traces.extend(v.iter());
}
}
}
if let Some(v) = rustc_attr_ir::find_attr!(tcx, did, CfgTrace(v) => v) {
traces.extend(v.iter());
}
if traces.is_empty() {
return None;
}
let sm = tcx.sess.source_map();
let mut parts = Vec::new();
let mut sites = Vec::new();
for (entry, sp) in traces {
// canonical text from rustc's own parse; a predicate this printer
// has no form for (`version(…)`) falls back to the source text
let text = cfg_entry_text(entry).map(|t| format!("cfg({t})")).or_else(|| {
let s = sm.span_to_snippet(*sp).ok()?;
let s = s.trim();
let s = s.strip_prefix("#![").or_else(|| s.strip_prefix("#[")).and_then(|x| x.strip_suffix(']')).unwrap_or(s);
Some(match mimir_driver::cfg_scan::parse_predicate(s) {
Some((t, _)) => t,
None => s.split_whitespace().collect::<Vec<_>>().join(" "),
})
});
let Some(text) = text.filter(|t| !t.is_empty()) else { continue };
parts.push(text);
let mut here = Vec::new();
cfg_entry_features(entry, false, &mut here);
sites.push((*sp, mimir_driver::cfg_scan::merge_features(here)));
}
if parts.is_empty() {
return None;
}
Some(Gate { text: parts.join(" && "), sites })
}
// ------------------------------------------------- visibility and docs
/// `(visibility, effective_visibility)` of a local def; `(None, None)` for
/// a kind rustc gives no visibility (impl blocks, generic params …).
fn visibility_of(&self, did: LocalDefId) -> (Option<String>, Option<EffectiveVisibility>) {
let tcx = self.tcx;
let d = did.to_def_id();
if !has_visibility(tcx.def_kind(d)) {
return (None, None);
}
let nominal = match tcx.visibility(d) {
ty::Visibility::Public => visibility::PUB.to_string(),
ty::Visibility::Restricted(m) => {
let m = m.to_def_id();
if m == tcx.parent_module_from_def_id(did).to_def_id() {
// its own module only: no qualifier, `pub(self)`, and at
// the crate root also `pub(crate)` — one meaning, one value
visibility::PRIVATE.to_string()
} else if m.is_crate_root() {
visibility::PUB_CRATE.to_string()
} else {
let path = tcx.def_path(m).to_string_no_crate_verbose();
visibility::pub_in(path.trim_start_matches("::"))
}
}
};
let effective = match tcx.effective_visibilities(()).public_at_level(did) {
Some(EffLevel::Direct) => EffectiveVisibility::Direct,
Some(EffLevel::Reexported) => EffectiveVisibility::Reexported,
Some(EffLevel::Reachable) => EffectiveVisibility::Reachable,
Some(EffLevel::ReachableThroughImplTrait) => EffectiveVisibility::ReachableImplTrait,
None => EffectiveVisibility::Internal,
};
(Some(nominal), Some(effective))
}
/// First line of the item's doc comment (`///`, `//!`, `/** */`, `#[doc = "…"]`).
fn doc_of(&self, did: LocalDefId) -> Option<String> {
let tcx = self.tcx;
tcx.hir_attrs(tcx.local_def_id_to_hir_id(did))
.iter()
.filter_map(|a| a.doc_str())
.find_map(|s| mimir_schema::doc_summary(s.as_str()))
}
// ---------------------------------------------------------------- emit
fn base_row(&mut self, did: DefId, k: &str, level: Level) -> NodeRow {
let ci = self.crate_info(did.krate);
let parent = self.node_parent(did).map(|p| self.moniker(p));
NodeRow {
id: self.moniker(did),
kind: k.to_string(),
level,
name: if did.is_crate_root() {
self.tcx.crate_name(did.krate).to_string()
} else {
self.name_of(did)
},
parent,
repo: if did.is_local() { self.local.repo.clone() } else { None },
krate: ci.package.clone(),
version: ci.version.clone(),
file: None,
start_line: None,
end_line: None,
cfg: None,
origin: ci.origin,
provenance: Provenance::SourceOnly,
valid_from_sha: self.local.sha.clone(),
valid_to_sha: None,
ty: None,
is_test: false,
defined_here: did.is_local(),
visibility: None,
effective_visibility: None,
doc: None,
has_doc: None,
ignored: None,
doc_hidden: None,
link_name: None,
}
}
fn push_node(&mut self, row: NodeRow) {
if self.node_ids.insert(row.id.clone()) {
self.nodes.push(row);
} else if row.defined_here
&& row.kind != kind::IMPL
// generics of merged inherent impl blocks
&& !(row.kind == kind::GENERIC && row.parent.as_deref().is_some_and(|p| p.ends_with(']')))
{
self.collisions += 1;
if self.collision_samples.len() < 20 {
let first = self.nodes.iter().find(|n| n.id == row.id);
self.collision_samples.push(format!(
"{} @{}:{} first @{}:{}",
row.id,
row.file.as_deref().unwrap_or("?"),
row.start_line.unwrap_or(0),
first.and_then(|n| n.file.as_deref()).unwrap_or("?"),
first.and_then(|n| n.start_line).unwrap_or(0),
));
}
}
}
/// One SEMANTIC edge per `(from, to, kind)`; every distinct site
/// `(file, line)` bumps its `count` and, when positioned, adds a span row.
fn push_edge(&mut self, from: String, to: String, kind: EdgeKind, file: Option<String>, line: Option<u32>) {
let idx = match self.edge_index.get(&(from.clone(), to.clone(), kind)) {
Some(i) => *i,
None => {
let i = self.edges.len();
self.edge_index.insert((from.clone(), to.clone(), kind), i);
self.edges.push(EdgeRow {
from: from.clone(),
to: to.clone(),
kind,
count: 0,
provenance: Provenance::SourceOnly,
valid_from_sha: self.local.sha.clone(),
valid_to_sha: None,
negated: None,
partial: None,
access: None,
});
i
}
};
if self.edge_sites.insert((idx, file.clone(), line)) {
self.edges[idx].count += 1;
if let (Some(file), Some(line)) = (file, line) {
self.edge_spans.push(SpanRow {
target: SPAN_EDGE,
key: edge_key(&from, &to, kind),
file,
start_line: line,
end_line: line,
});
}
}
}
/// Edge to an arbitrary DefId target; extern targets get a reference stub
/// node (defined_here = false) so the dump is self-describing.
fn edge_to(&mut self, from: DefId, to: DefId, kind: EdgeKind, site: Option<Span>) {
let Some(to) = self.node_or_ancestor(to) else { return };
let fm = self.moniker(from);
let tm = self.moniker(to);
if !to.is_local() {
self.cross_crate += 1;
let tci = self.crate_info(to.krate);
if self.local.origin == Origin::Local && tci.origin != Origin::Local {
self.cross_repo += 1;
}
self.stub(to);
}
let (file, line) = match site {
Some(sp) => {
let (f, l, _) = self.file_lines(sp, true);
(f, l)
}
None => (None, None),
};
self.push_edge(fm, tm, kind, file, line);
}
fn stub(&mut self, did: DefId) {
if did.is_local() || !self.stub_done.insert(did) {
return;
}
let (k, level) = self.node_kind(did).unwrap_or(("item", Level::Item));
let mut row = self.base_row(did, k, level);
if !did.is_crate_root() {
let (f, s, e) = self.file_lines(self.tcx.def_span(did), false);
row.file = f;
row.start_line = s;
row.end_line = e;
}
self.push_node(row);
}
// ------------------------------------------------------------- passes
/// `#[test]` fns: in a `--test` build each has a sibling const carrying
/// `#[rustc_test_marker]` with the same name (clippy's idiom).
fn collect_tests(&mut self) {
let tcx = self.tcx;
let mut marked: HashSet<(DefId, String)> = HashSet::new();
for did in tcx.iter_local_def_id() {
if self.is_test_marker(did) {
marked.insert((tcx.parent(did.to_def_id()), self.name_of(did.to_def_id())));
}
}
if marked.is_empty() {
return;
}
for did in tcx.iter_local_def_id() {
if tcx.def_kind(did) == DefKind::Fn
&& marked.contains(&(tcx.parent(did.to_def_id()), self.name_of(did.to_def_id())))
{
self.test_fns.insert(did);
}
}
}
/// The `#[rustc_test_marker]` const/static the test harness generates per
/// `#[test]` fn (a `TestDescAndFn`).
fn is_test_marker(&self, did: LocalDefId) -> bool {
let tcx = self.tcx;
matches!(tcx.def_kind(did), DefKind::Const { .. } | DefKind::Static { .. })
&& rustc_attr_ir::find_attr!(tcx, did, RustcTestMarker(_))
}
/// Items the `--test` harness synthesises (its `main`, re-export modules):
/// not the program, never nodes.
fn is_harness_item(&self, did: LocalDefId) -> bool {
let sp = self.tcx.source_span(did);
sp.from_expansion()
&& matches!(
sp.ctxt().outer_expn_data().kind,
ExpnKind::AstPass(AstPass::TestHarness)
)
}
/// The symbol a foreign fn / static is LINKED as (schema 4): rustc's own
/// symbol name of the item, which is the item's name unless `#[link_name]`
/// says otherwise. Under `catch_unwind` like every symbol-name query here:
/// a panic leaves the column null, never a made-up name.
fn link_name_of(&self, d: DefId) -> Option<String> {
let tcx = self.tcx;
std::panic::catch_unwind(AssertUnwindSafe(|| tcx.symbol_name(Instance::mono(tcx, d)).name.to_string())).ok()
}
fn walk_defs(&mut self) {
let tcx = self.tcx;
let ids: Vec<LocalDefId> = tcx.iter_local_def_id().collect();
for did in ids {
let d = did.to_def_id();
let Some((k, level)) = self.node_kind(d) else { continue };
if self.is_test_marker(did) || self.is_harness_item(did) {
continue;
}
if !d.is_crate_root() && tcx.opt_item_name(d).is_none() && !matches!(tcx.def_kind(d), DefKind::Impl { .. }) {
continue;
}
let mut row = self.base_row(d, k, level);
let span = match tcx.def_kind(d) {
DefKind::Mod => tcx.hir_get_module(LocalModId::new_unchecked(did)).0.spans.inner_span,
_ => tcx.source_span(did),
};
let (f, s, e) = self.file_lines(span, true);
row.file = f;
row.start_line = s;
row.end_line = e;
let gate = self.cfg_of(did);
row.cfg = gate.as_ref().map(|g| g.text.clone());
row.is_test = self.test_fns.contains(&did);
row.ty = self.printed_ty(d);
let (vis, eff) = self.visibility_of(did);
row.visibility = vis;
row.effective_visibility = eff;
if k != kind::GENERIC {
row.doc = self.doc_of(did);
row.has_doc = Some(row.doc.is_some());
row.doc_hidden = Some(tcx.is_doc_hidden(d));
}
if row.is_test {
row.ignored = Some(rustc_attr_ir::find_attr!(tcx, did, Ignore { .. }));
}
if k == kind::FFI {
row.link_name = self.link_name_of(d);
}
// the nameable API in rustc's `missing_docs` sense: impl blocks
// and the items of a trait impl are not documented on their own
let in_trait_impl = tcx.opt_parent(d).is_some_and(|p| matches!(tcx.def_kind(p), DefKind::Impl { of_trait: true }));
if eff.is_some_and(EffectiveVisibility::is_public_api) && !in_trait_impl {
self.x.api_items += 1;
self.x.api_documented += usize::from(row.has_doc == Some(true));
}
let id = row.id.clone();
let parent = row.parent.clone();
self.push_node(row);
if let Some(p) = parent {
self.push_edge(p, id.clone(), EdgeKind::Contains, None, None);
}
if let Some(g) = &gate {
self.gate_edges(&id, g);
}
match tcx.def_kind(d) {
DefKind::Impl { .. } => self.impl_edges(d),
DefKind::Fn | DefKind::AssocFn => self.fn_parts(did, &id),
DefKind::Field => {
let fty = tcx.type_of(d).instantiate_identity().skip_normalization();
for adt in adts_in(fty) {
self.edge_to(d, adt, EdgeKind::UsesType, None);
}
}
_ => {}
}
// EXPANDS: an item produced by a macro expansion → that macro
if level == Level::Item && span.from_expansion() {
if let Some(mac) = span.ctxt().outer_expn_data().macro_def_id {
self.edge_to(d, mac, EdgeKind::Expands, None);
}
}
}
}
fn printed_ty(&self, did: DefId) -> Option<String> {
let tcx = self.tcx;
match tcx.def_kind(did) {
DefKind::Field | DefKind::Const { .. } | DefKind::Static { .. } | DefKind::TyAlias | DefKind::AssocConst { .. } => {
Some(plain_ty(tcx.type_of(did).instantiate_identity().skip_normalization()))
}
_ => None,
}
}
fn impl_edges(&mut self, impl_id: DefId) {
let tcx = self.tcx;
if let Some(t) = tcx.impl_opt_trait_id(impl_id) {
self.edge_to(impl_id, t, EdgeKind::Implements, None);
}
let self_ty = tcx.type_of(impl_id).instantiate_identity().skip_normalization();
if let ty::Adt(adt, _) = peel_refs(self_ty).kind() {
self.edge_to(impl_id, adt.did(), EdgeKind::ForType, None);
}
}
/// A PART row of this crate under `parent` (a ret / a local): identity
/// columns, the printed type, no span (the caller sets it).
fn part_row(&mut self, id: String, k: &str, name: String, parent: &str, ty: String) -> NodeRow {
let ci = self.crate_info(LOCAL_CRATE);
let mut row = NodeRow::new(id, k, Level::Part, name, ci.package.clone(), ci.version.clone(), ci.origin);
row.parent = Some(parent.to_string());
row.repo = self.local.repo.clone();
row.valid_from_sha = self.local.sha.clone();
row.ty = Some(ty);
row
}
/// A fn's `let` bindings as `local` PART nodes (see [`kind::LOCAL`]), at
/// most [`mimir_schema::LOCALS_PER_FN`].
fn fn_locals(&mut self, fn_id: &str, locals: &[(String, String, Span)]) {
let mut seen = HashSet::new();
for (name, ty, span) in locals {
if seen.len() >= mimir_schema::LOCALS_PER_FN {
break;
}
let (f, s, e) = self.file_lines(*span, true);
let Some(line) = s else { continue };
if !seen.insert((name.clone(), line)) {
continue;
}
let mut desc = String::new();
moniker::push_descriptor(&mut desc, Suffix::Term, &format!("{name}@{line}"));
let mut row = self.part_row(format!("{fn_id}{desc}"), kind::LOCAL, name.clone(), fn_id, ty.clone());
(row.file, row.start_line, row.end_line) = (f, s, e);
let id = row.id.clone();
self.push_node(row);
self.push_edge(fn_id.to_string(), id, EdgeKind::Contains, None, None);
}
}
/// Params (with types) as PART nodes + USES_TYPE fn → ADTs of the signature.
fn fn_parts(&mut self, did: LocalDefId, fn_id: &str) {
let tcx = self.tcx;
let d = did.to_def_id();
let sig = tcx.fn_sig(d).instantiate_identity().skip_normalization().skip_binder();
let idents = tcx.fn_arg_idents(d);
let ci = self.crate_info(LOCAL_CRATE);
let mut adts: Vec<DefId> = Vec::new();
for (i, input) in sig.inputs().iter().enumerate() {
let ident = idents.get(i).copied().flatten();
let pname = ident
.map(|id| id.name.to_string())
.filter(|n| !n.is_empty() && n != "_")
.unwrap_or_else(|| format!("_{i}"));
let mut desc = String::new();
moniker::push_descriptor(&mut desc, Suffix::Parameter, &pname);
let pid = format!("{fn_id}{desc}");
let (f, s, e) = match ident {
Some(id) => self.file_lines(id.span, true),
None => (None, None, None),
};
let row = NodeRow {
id: pid.clone(),
kind: kind::PARAM.into(),
level: Level::Part,
name: pname,
parent: Some(fn_id.to_string()),
repo: self.local.repo.clone(),
krate: ci.package.clone(),
version: ci.version.clone(),
file: f,
start_line: s,
end_line: e,
cfg: None,
origin: ci.origin,
provenance: Provenance::SourceOnly,
valid_from_sha: self.local.sha.clone(),
valid_to_sha: None,
ty: Some(plain_ty(*input)),
is_test: false,
defined_here: true,
visibility: None,
effective_visibility: None,
doc: None,
has_doc: None,
ignored: None,
doc_hidden: None,
link_name: None,
};
self.push_node(row);
self.push_edge(fn_id.to_string(), pid, EdgeKind::Contains, None, None);
adts.extend(adts_in(*input));
}
adts.extend(adts_in(sig.output()));
// the return type: a part under the fn (none for `()`)
if !sig.output().is_unit() {
let mut desc = String::new();
moniker::push_descriptor(&mut desc, Suffix::Term, "->");
let mut row = self.part_row(format!("{fn_id}{desc}"), kind::RET, "->".to_string(), fn_id, plain_ty(sig.output()));
if let Some(decl) = tcx.hir_fn_decl_by_hir_id(tcx.local_def_id_to_hir_id(did)) {
(row.file, row.start_line, row.end_line) = self.file_lines(decl.output.span(), true);
}
let rid = row.id.clone();
self.push_node(row);
self.push_edge(fn_id.to_string(), rid, EdgeKind::Contains, None, None);
}
let mut seen = HashSet::new();
for a in adts {
if seen.insert(a) {
self.edge_to(d, a, EdgeKind::UsesType, None);
}
}
}
fn walk_crate_deps(&mut self) {
let tcx = self.tcx;
let root = LOCAL_CRATE.as_def_id();
for &cnum in tcx.crates(()) {
if tcx.extern_crate(cnum).is_some_and(|e| e.is_direct()) {
self.edge_to(root, cnum.as_def_id(), EdgeKind::DependsOn, None);
}
}
}
/// Remember who calls what — as written and as resolved — for the table
/// pass (only when it will run).
fn note_calls(&mut self, owner: LocalDefId, calls: &[(DefId, DefId)]) {
if !self.tables.enabled {
return;
}
for (written, resolved) in calls {
for did in [written, resolved] {
let v = self.tables.callers.entry(*did).or_default();
if v.last() != Some(&owner) {
v.push(owner);
}
}
}
}
/// [`Self::walk_bodies`]' call collection alone (no edges): what the
/// table pass needs when the crate is not dumped.
fn collect_calls(&mut self) {
let tcx = self.tcx;
let owners: Vec<LocalDefId> = tcx.hir_body_owners().collect();
for owner in owners {
if tcx.is_typeck_child(owner.to_def_id()) || self.is_test_marker(owner) || self.is_harness_item(owner) {
continue;
}
let mut v = BodyVisitor { tcx, typeck: tcx.typeck(owner), root: owner, typing_env: TypingEnv::post_analysis(tcx, owner.to_def_id()), found: Vec::new(), calls: Vec::new(), locals: Vec::new(), callees: Default::default() };
v.visit_body(tcx.hir_body_owned_by(owner));
self.note_calls(owner, &v.calls);
}
}
/// [`Self::init_tables`], never taking the dump down with it.
fn init_tables_guarded(&mut self) {
if let Err(p) = std::panic::catch_unwind(AssertUnwindSafe(|| self.init_tables())) {
let msg = p.downcast_ref::<String>().cloned().or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default();
self.tables.enabled = false;
self.tables.error = Some(format!("panic at start: {msg}"));
self.table_pass_failed(&format!("panic at start: {msg}"));
}
}
/// The table pass, never taking the rest of the dump down with it: a
/// panic in it loses the TOUCHES_TABLE edges of this crate and is written
/// into the stats (`table_pass_error`).
fn walk_tables_guarded(&mut self) {
let (nodes, edges, spans) = (self.nodes.len(), self.edges.len(), self.edge_spans.len());
if let Err(p) = std::panic::catch_unwind(AssertUnwindSafe(|| self.walk_tables(true))) {
let msg = p.downcast_ref::<String>().cloned().or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default();
// half-written rows of the pass go: every row it adds is appended
for n in self.nodes.drain(nodes..) {
self.node_ids.remove(&n.id);
}
for e in self.edges.drain(edges..) {
self.edge_index.remove(&(e.from, e.to, e.kind));
}
self.edge_sites.retain(|(i, _, _)| *i < edges);
self.edge_spans.truncate(spans);
self.tables.error = Some(format!("panic: {msg}"));
self.table_pass_failed(&format!("panic: {msg}"));
}
}
fn walk_bodies(&mut self) {
let tcx = self.tcx;
let owners: Vec<LocalDefId> = tcx.hir_body_owners().collect();
for owner in owners {
if tcx.is_typeck_child(owner.to_def_id())
|| self.is_test_marker(owner)
|| self.is_harness_item(owner)
{
continue;
}
let Some(from) = self.node_or_ancestor(owner.to_def_id()) else { continue };
let typeck = tcx.typeck(owner);
let body = tcx.hir_body_owned_by(owner);
let mut v = BodyVisitor {
tcx,
typeck,
root: owner,
typing_env: TypingEnv::post_analysis(tcx, owner.to_def_id()),
found: Vec::new(),
calls: Vec::new(),
locals: Vec::new(),
callees: Default::default(),
};
v.visit_body(body);
self.note_calls(owner, &v.calls);
if from == owner.to_def_id() && matches!(tcx.def_kind(from), DefKind::Fn | DefKind::AssocFn) && !v.locals.is_empty() {
let fn_id = self.moniker(from);
self.fn_locals(&fn_id, &v.locals);
}
let is_test = from.as_local().is_some_and(|l| self.test_fns.contains(&l));
for (target, kind, span) in v.found {
self.edge_to(from, target, kind, Some(span));
if is_test && kind == EdgeKind::Calls {
let local_target = target.is_local() || self.crate_info(target.krate).origin == Origin::Local;
if local_target {
self.edge_to(from, target, EdgeKind::Tests, Some(span));
}
}
}
}
}
}
/// The def kinds rustc computes a visibility for (`tcx.visibility` panics on
/// the rest — generic params, impl blocks, closures …).
fn has_visibility(dk: DefKind) -> bool {
matches!(
dk,
DefKind::Mod
| DefKind::Struct
| DefKind::Union
| DefKind::Enum
| DefKind::Variant
| DefKind::Trait
| DefKind::TyAlias
| DefKind::ForeignTy
| DefKind::TraitAlias
| DefKind::AssocTy
| DefKind::Fn
| DefKind::Const { .. }
| DefKind::Static { nested: false, .. }
| DefKind::AssocFn
| DefKind::AssocConst { .. }
| DefKind::Macro(_)
| DefKind::Field
)
}
/// One parsed cfg predicate in canonical spelling (`all(feature = "a",
/// not(unix))`) — the spelling `cfg_scan` gives a compiled-out item's
/// predicate, so the same attribute reads the same on both sides. `None` for
/// a form with no canonical spelling here (`version(…)`).
fn cfg_entry_text(e: &CfgEntry) -> Option<String> {
let list = |v: &[CfgEntry]| v.iter().map(cfg_entry_text).collect::<Option<Vec<_>>>().map(|p| p.join(", "));
Some(match e {
CfgEntry::All(v, _) => format!("all({})", list(v)?),
CfgEntry::Any(v, _) => format!("any({})", list(v)?),
CfgEntry::Not(inner, _) => format!("not({})", cfg_entry_text(inner)?),
CfgEntry::Bool(b, _) => b.to_string(),
CfgEntry::NameValue { name, value: Some(v), .. } => format!("{name} = {:?}", v.as_str()),
CfgEntry::NameValue { name, value: None, .. } => name.to_string(),
CfgEntry::Version(..) => return None,
})
}
/// Every `feature = "…"` of a parsed cfg predicate with its polarity.
fn cfg_entry_features(e: &CfgEntry, negated: bool, out: &mut Vec<(String, bool)>) {
match e {
CfgEntry::All(v, _) | CfgEntry::Any(v, _) => v.iter().for_each(|x| cfg_entry_features(x, negated, out)),
CfgEntry::Not(inner, _) => cfg_entry_features(inner, !negated, out),
CfgEntry::NameValue { name, value: Some(v), .. } if *name == rustc_span::sym::feature => out.push((v.to_string(), negated)),
_ => {}
}
}
/// Human type string for the `ty` column (`Vec<Point>`): printed with FULL
/// paths and cut by [`mimir_driver::shorten_paths`]. Never the compiler's own
/// trimming — its trimmed_def_paths query makes rustc ICE at the end of a
/// session that emitted no diagnostic (toml_edit, dwarf 2026-10-17); the
/// guard test `no_trimmed_paths` keeps it so.
fn plain_ty(t: Ty<'_>) -> String {
let _g = NoTrimmedGuard::new();
mimir_driver::shorten_paths(&t.to_string())
}
fn is_param_arg(a: ty::GenericArg<'_>) -> bool {
match a.kind() {
GenericArgKind::Type(t) => matches!(t.kind(), ty::Param(_)),
GenericArgKind::Lifetime(_) => true,
GenericArgKind::Const(c) => matches!(c.kind(), ty::ConstKind::Param(_)),
}
}
fn peel_refs(mut t: Ty<'_>) -> Ty<'_> {
while let ty::Ref(_, inner, _) = t.kind() {
t = *inner;
}
t
}
fn adts_in(t: Ty<'_>) -> Vec<DefId> {
let mut v = Vec::new();
for arg in t.walk() {
if let GenericArgKind::Type(ty) = arg.kind() {
if let ty::Adt(adt, _) = ty.kind() {
if !v.contains(&adt.did()) {
v.push(adt.did());
}
}
}
}
v
}
/// Walks one typeck root's body (closures and inline consts included — they
/// share its typeck results) and collects CALLS + READS_FIELD targets.
struct BodyVisitor<'a, 'tcx> {
tcx: TyCtxt<'tcx>,
typeck: &'a ty::TypeckResults<'tcx>,
root: LocalDefId,
typing_env: TypingEnv<'tcx>,
found: Vec<(DefId, EdgeKind, Span)>,
/// every call: (the callee as written, as resolved)
calls: Vec<(DefId, DefId)>,
/// every `let` binding: (name, printed type, its ident's span)
locals: Vec<(String, String, Span)>,
/// the callee paths of the calls seen (`f` in `f(x)`): a path to a fn
/// that is NOT one of them names the fn as a VALUE (`each_addr(a, f)`)
callees: std::collections::HashSet<hir::HirId>,
}
impl<'a, 'tcx> BodyVisitor<'a, 'tcx> {
/// A trait method with concrete args → the impl method rustc would pick
/// (`Instance::try_resolve`); anything generic / dyn / shim stays the
/// trait's own method.
fn resolve(&self, did: DefId, hir_id: hir::HirId) -> DefId {
let tcx = self.tcx;
if tcx.trait_of_assoc(did).is_none() {
return did;
}
let Some(args) = self.typeck.node_args_opt(hir_id) else { return did };
if args.has_non_region_param() || args.has_infer() || args.references_error() || args.has_escaping_bound_vars() {
return did;
}
let args = tcx.erase_and_anonymize_regions(args);
match Instance::try_resolve(tcx, self.typing_env, did, args) {
Ok(Some(inst)) => match inst.def {
ty::InstanceKind::Item(d) => d,
_ => did,
},
_ => did,
}
}
}
impl<'a, 'tcx> Visitor<'tcx> for BodyVisitor<'a, 'tcx> {
type NestedFilter = nested_filter::OnlyBodies;
fn maybe_tcx(&mut self) -> TyCtxt<'tcx> {
self.tcx
}
fn visit_nested_body(&mut self, id: hir::BodyId) {
// only bodies that share THIS typeck root (closures, inline consts);
// anon consts etc. are their own roots, walked on their own.
let owner = self.tcx.hir_body_owner_def_id(id);
if self.tcx.typeck_root_def_id_local(owner) == self.root {
let body = self.tcx.hir_body(id);
self.visit_body(body);
}
}
fn visit_local(&mut self, l: &'tcx hir::LetStmt<'tcx>) {
if !l.span.from_expansion() {
l.pat.each_binding(|_, hir_id, _, ident| {
if ident.name.as_str().starts_with('_') || ident.span.from_expansion() {
return;
}
if let Some(t) = self.typeck.node_type_opt(hir_id) {
self.locals.push((ident.name.to_string(), plain_ty(t), ident.span));
}
});
}
intravisit::walk_local(self, l);
}
fn visit_expr(&mut self, e: &'tcx hir::Expr<'tcx>) {
match e.kind {
hir::ExprKind::Call(callee, _) => {
let mut named = false;
if let hir::ExprKind::Path(ref qpath) = callee.kind {
self.callees.insert(callee.hir_id);
if let Res::Def(DefKind::Fn | DefKind::AssocFn, did) =
self.typeck.qpath_res(qpath, callee.hir_id)
{
named = true;
let t = self.resolve(did, callee.hir_id);
self.calls.push((did, t));
self.found.push((t, EdgeKind::Calls, e.span));
}
}
if !named {
// a call through a VALUE: a closure, a `Box<dyn Fn…>`, a
// generic `F: Fn…` is an overloaded call of the `Fn*`
// trait method (resolved to the impl when the type is
// concrete); a fn pointer implements `Fn`, so its call is
// recorded as `Fn::call` — the reader widens both
if let Some(did) = self.typeck.type_dependent_def_id(e.hir_id) {
let t = self.resolve(did, e.hir_id);
self.calls.push((did, t));
self.found.push((t, EdgeKind::Calls, e.span));
} else if self.typeck.expr_ty_adjusted_opt(callee).is_some_and(|t| matches!(t.kind(), ty::FnPtr(..)))
&& let Some(call) = self.tcx.lang_items().fn_trait().and_then(|tr| self.tcx.associated_item_def_ids(tr).first().copied())
{
self.found.push((call, EdgeKind::Calls, e.span));
}
}
}
// a fn named as a VALUE (`each_addr(addr, TcpStream::connect)`,
// `.map(Self::parse)`, a fn pointer stored): whoever receives it
// calls it, so the body that names it reaches it — a CALLS edge
// (the call through the generic / pointer has no resolvable callee)
hir::ExprKind::Path(ref qpath) if !self.callees.contains(&e.hir_id) => {
if let Res::Def(DefKind::Fn | DefKind::AssocFn, did) = self.typeck.qpath_res(qpath, e.hir_id) {
let t = self.resolve(did, e.hir_id);
self.found.push((t, EdgeKind::Calls, e.span));
}
}
hir::ExprKind::MethodCall(..) => {
if let Some(did) = self.typeck.type_dependent_def_id(e.hir_id) {
let t = self.resolve(did, e.hir_id);
self.calls.push((did, t));
self.found.push((t, EdgeKind::Calls, e.span));
}
}
hir::ExprKind::Field(base, _) => {
if let (Some(idx), Some(bt)) =
(self.typeck.opt_field_index(e.hir_id), self.typeck.expr_ty_adjusted_opt(base))
{
if let ty::Adt(adt, _) = peel_refs(bt).kind() {
if adt.is_struct() || adt.is_union() {
if let Some(f) = adt.non_enum_variant().fields.get(idx) {
self.found.push((f.did, EdgeKind::ReadsField, e.span));
}
}
}
}
}
_ => {}
}
intravisit::walk_expr(self, e);
}
}