astral-sh/ruff · error
extra use-def data should have been retained
Error message
extra use-def data should have been retained
What it means
UseDefMap stores rarely used tables (bindings-by-use, member place states, enclosing snapshots, loop headers) in an optional extra box that is only allocated when at least one of those collections is non-empty when the map finishes building (use_def.rs:2925-2937). extra() panics when an accessor that reads those tables (loop_header, member end-of-scope queries, bindings-by-use lookups) is called on a map built with extra == None.
Solutions
- Re-derive LoopHeaderId/ScopedPlaceId/use IDs from the current semantic model instead of caching them across revisions, so IDs and the map they index come from the same snapshot
- Guard the query: only call extra-backed accessors for constructs that exist in this file's current semantic index (e.g., only for loops actually present)
- If the ID provably came from the same map, capture the backtrace and file a ty issue: the map handed out an ID whose backing table it did not retain
Defensive patterns
Strategy: validation
Validate before calling
// Re-derive IDs from the current snapshot instead of caching them: let index = semantic_index(db, file); // current revision let loop_id = index.loop_header(loop_node); // matches this UseDefMap let header = use_def_map.loop_header(loop_id);
Prevention
- Never cache LoopHeaderId, ScopedPlaceId, or use IDs across Salsa revisions or edits; re-derive them per query
- Only call extra-backed accessors (loop_header, member end-of-scope queries) for constructs present in the current file's semantic index
- When adding new UseDefMap queries, assert the corresponding table exists for the file shape before reading it
When it happens
Trigger: Calling UseDefMap::loop_header(LoopHeaderId), end_of_scope_declarations on a member place, or a bindings-by-use lookup on a map whose four extra collections were all empty at finish time. In practice this means the ID being looked up came from a different or stale use-def map revision, or a builder path recorded IDs but dropped their table.
Common situations: Editor/LSP flows where a cached LoopHeaderId or ScopedPlaceId outlives the Salsa revision that produced it after an edit; new query code that assumes a table always exists without checking that the module contains loops, member places, or multi-bindings.
Related errors
- binding definition should have retained declarations
- Expected live-declarations length to fit into a u32
- accumulator should not be empty
- anchor should not be NodeIndex::NONE
- argument index should be valid
AI-assisted analysis of astral-sh/ruff@5bb508f594 (2026-08-20).
Data as JSON: /api/errors/d6bc26410d8ea977.
Report an issue: GitHub.
Appendix: source
Thrown at crates/ty_python_core/src/use_def.rs:903
}
fn intern_table<T: Eq + std::hash::Hash>(
values: &mut FxHashSet<Arc<T>>,
value: Arc<T>,
) -> Arc<T> {
if let Some(existing) = values.get(value.as_ref()) {
Arc::clone(existing)
} else {
values.insert(Arc::clone(&value));
value
}
}
}
/// Information about a given range of source code.
#[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)]
struct RangeInfo {
reachability: ScopedReachabilityConstraintId,
in_type_checking_block: bool,
}
impl Default for RangeInfo {
fn default() -> Self {
Self {
reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE,
in_type_checking_block: false,
}
}
}
#[derive(Debug, PartialEq, Eq, get_size2::GetSize)]
struct MultiBindingsByUse(ThinVec<(ScopedUseId, Box<[Bindings]>)>);
impl MultiBindingsByUse {
fn from_map(map: FxHashMap<ScopedUseId, Vec<Bindings>>) -> Self {
let mut entries = ThinVec::with_capacity(map.len());View on GitHub (pinned to 5bb508f594)