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.

Source

Thrown at crates/ty_python_core/src/use_def.rs:870

    }
}

pub enum ApplicableConstraints<'map, 'db> {
    UnboundBinding(NarrowingEvaluator<'map, 'db>),
    ConstrainedBindings(BindingWithConstraintsIterator<'map, 'db>),
}

impl<'db> UseDefMap<'db> {
    fn constraint_tables(&self) -> &ConstraintTables<'db> {
        self.constraint_tables
            .as_deref()
            .map_or(&EMPTY_CONSTRAINT_TABLES, |tables| tables)
    }

    fn extra(&self) -> &UseDefMapExtra {
        self.extra
            .as_deref()
            .expect("extra use-def data should have been retained")
    }

    pub fn loop_header(&self, id: LoopHeaderId) -> &LoopHeader {
        &self.extra().loop_headers[id]
    }

    pub fn reachability_constraints(&self) -> &ReachabilityConstraints {
        &self.constraint_tables().reachability_constraints
    }

    pub fn predicates(&self) -> &Predicates<'db> {
        &self.constraint_tables().predicates
    }

    pub fn range_reachability(
        &self,
    ) -> impl Iterator<Item = (TextRange, ScopedReachabilityConstraintId)> + '_ {
        self.range_reachability

View on GitHub (pinned to d1087a4b9e)

Solutions

  1. 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
  2. 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)
  3. 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

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


AI-assisted analysis of astral-sh/ruff@d1087a4b9e (2026-08-20). Data as JSON: /api/errors/d6bc26410d8ea977. Report an issue: GitHub.