astral-sh/ruff · error

binding definition should have retained declarations

Error message

binding definition should have retained declarations

What it means

The use-def map stores two flavors of entry per definition: record_binding retains the declarations visible at the binding (declarations: Some, use_def.rs:1977) while record_declaration stores None for declaration-only definitions such as the declaring phase of an annotated assignment (use_def.rs:2374). declarations_at_binding unwraps the Some case because it is only meant to be called with definitions that bind a value; the expect fires when a declaration-only definition is passed instead.

Source

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

        nested_laziness: ScopeLaziness,
    ) -> EnclosingSnapshotResult<'_, 'db> {
        let boundness_analysis = if nested_laziness.is_eager() {
            BoundnessAnalysis::BasedOnUnboundVisibility
        } else {
            // TODO: We haven't implemented proper boundness analysis for nonlocal symbols, so we assume the boundness is bound for now.
            BoundnessAnalysis::AssumeBound
        };

        let Some(extra) = self.extra.as_deref() else {
            return EnclosingSnapshotResult::NotFound;
        };

        match extra.enclosing_snapshots.get(snapshot_id) {
            Some(InternedEnclosingSnapshotId::Constraint(constraint)) => {
                EnclosingSnapshotResult::FoundConstraint(*constraint)
            }
            Some(InternedEnclosingSnapshotId::Bindings(bindings_id)) => {
                EnclosingSnapshotResult::FoundBindings(
                    self.bindings_iterator(
                        &self.interned_bindings[*bindings_id],
                        boundness_analysis,
                    ),
                )
            }
            None => EnclosingSnapshotResult::NotFound,
        }
    }

    pub fn bindings_at_definition(
        &self,
        definition: Definition<'db>,
    ) -> BindingWithConstraintsIterator<'_, 'db> {
        let bindings = self.definitions_by_definition.get(&definition).map_or_else(
            || ALWAYS_UNBOUND_BINDINGS.as_slice(),
            |definitions| &self.interned_bindings[definitions.bindings],
        );

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. Verify the caller passes a binding Definition obtained from the bindings API (bindings_iterator and friends), not an arbitrary definition from the index
  2. If a new definition kind was added, route it through record_binding (or the combined declare-and-bind path) so declarations are retained for it
  3. Otherwise minimize the module and file a ty issue: the definition classification for that syntax node is wrong
Defensive patterns

Strategy: validation

Validate before calling

// Only query declarations at definitions that actually bind:
for binding in use_def_map.all_bindings(db, file) {
    let decls = use_def_map.declarations_at_binding(binding);
}

Prevention

When it happens

Trigger: Calling declarations_at_binding with a Definition that was classified as a declaration rather than a binding, e.g., the declare-only half of an annotated assignment like `x: int` with no right-hand side, or any newly added definition kind routed through record_declaration but later queried through the binding API.

Common situations: Contributor changes that add a new definition kind or alter the declare-vs-bind split of annotated assignments without updating the retention path; semantic query code that iterates all definitions instead of only binding definitions.

Related errors


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