[red-knot] Track reachability of scopes (#17332)

## Summary

Track the reachability of nested scopes within their parent scopes. We
use this as an additional requirement for emitting
`unresolved-reference` diagnostics (and in the future,
`unresolved-attribute` and `unresolved-import`). This means that we only
emit `unresolved-reference` for a given use of a symbol if the use
itself is reachable (within its own scope), *and if the scope itself is
reachable*. For example, no diagnostic should be emitted for the use of
`x` here:

```py
if False:
    x = 1

    def f():
        print(x)  # this use of `x` is reachable inside the `f` scope,
                  # but the whole `f` scope is not reachable.
```

There are probably more fine-grained ways of solving this problem, but
they require a more sophisticated understanding of nested scopes (see
#15777, in particular
https://github.com/astral-sh/ruff/issues/15777#issuecomment-2788950267).
But it doesn't seem completely unreasonable to silence *this specific
kind of error* in unreachable scopes.

## Test Plan

Observed changes in reachability tests and ecosystem.
This commit is contained in:
David Peter 2025-04-10 13:56:40 +02:00 committed by GitHub
parent 06ffeb2e09
commit 4d50ee6f52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 79 additions and 24 deletions

View file

@ -129,7 +129,11 @@ impl<'db> SemanticIndexBuilder<'db> {
eager_bindings: FxHashMap::default(),
};
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
builder.push_scope_with_parent(
NodeWithScopeRef::Module,
None,
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
builder
}
@ -191,17 +195,28 @@ impl<'db> SemanticIndexBuilder<'db> {
fn push_scope(&mut self, node: NodeWithScopeRef) {
let parent = self.current_scope();
self.push_scope_with_parent(node, Some(parent));
let reachabililty = self.current_use_def_map().reachability;
self.push_scope_with_parent(node, Some(parent), reachabililty);
}
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
fn push_scope_with_parent(
&mut self,
node: NodeWithScopeRef,
parent: Option<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
) {
let children_start = self.scopes.next_index() + 1;
// SAFETY: `node` is guaranteed to be a child of `self.module`
#[allow(unsafe_code)]
let node_with_kind = unsafe { node.to_kind(self.module.clone()) };
let scope = Scope::new(parent, node_with_kind, children_start..children_start);
let scope = Scope::new(
parent,
node_with_kind,
children_start..children_start,
reachability,
);
self.try_node_context_stack_manager.enter_nested_scope();
let file_scope_id = self.scopes.push(scope);