mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:15:12 +00:00
[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:
parent
06ffeb2e09
commit
4d50ee6f52
6 changed files with 79 additions and 24 deletions
|
@ -423,14 +423,10 @@ if False:
|
|||
x = 1
|
||||
|
||||
def f():
|
||||
# TODO
|
||||
# error: [unresolved-reference]
|
||||
print(x)
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
# TODO
|
||||
# error: [unresolved-reference]
|
||||
print(x)
|
||||
```
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ use salsa::Update;
|
|||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIds;
|
||||
use crate::semantic_index::ast_ids::{AstIds, ScopedUseId};
|
||||
use crate::semantic_index::attribute_assignment::AttributeAssignments;
|
||||
use crate::semantic_index::builder::SemanticIndexBuilder;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
|
||||
|
@ -240,6 +240,43 @@ impl<'db> SemanticIndex<'db> {
|
|||
Some(&self.scopes[self.parent_scope_id(scope_id)?])
|
||||
}
|
||||
|
||||
fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool {
|
||||
self.parent_scope_id(scope_id)
|
||||
.is_none_or(|parent_scope_id| {
|
||||
if !self.is_scope_reachable(db, parent_scope_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parent_use_def = self.use_def_map(parent_scope_id);
|
||||
let reachability = self.scope(scope_id).reachability();
|
||||
|
||||
parent_use_def.is_reachable(db, reachability)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if a given 'use' of a symbol is reachable from the start of the scope.
|
||||
/// For example, in the following code, use `2` is reachable, but `1` and `3` are not:
|
||||
/// ```py
|
||||
/// def f():
|
||||
/// x = 1
|
||||
/// if False:
|
||||
/// x # 1
|
||||
/// x # 2
|
||||
/// return
|
||||
/// x # 3
|
||||
/// ```
|
||||
pub(crate) fn is_symbol_use_reachable(
|
||||
&self,
|
||||
db: &'db dyn crate::Db,
|
||||
scope_id: FileScopeId,
|
||||
use_id: ScopedUseId,
|
||||
) -> bool {
|
||||
self.is_scope_reachable(db, scope_id)
|
||||
&& self
|
||||
.use_def_map(scope_id)
|
||||
.is_symbol_use_reachable(db, use_id)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the descendent scopes of `scope`.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,6 +12,7 @@ use rustc_hash::FxHasher;
|
|||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId;
|
||||
use crate::semantic_index::{semantic_index, SymbolMap};
|
||||
use crate::Db;
|
||||
|
||||
|
@ -176,6 +177,7 @@ pub struct Scope {
|
|||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendants: Range<FileScopeId>,
|
||||
reachability: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
|
@ -183,11 +185,13 @@ impl Scope {
|
|||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendants: Range<FileScopeId>,
|
||||
reachability: ScopedVisibilityConstraintId,
|
||||
) -> Self {
|
||||
Scope {
|
||||
parent,
|
||||
node,
|
||||
descendants,
|
||||
reachability,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,6 +218,10 @@ impl Scope {
|
|||
pub(crate) fn is_eager(&self) -> bool {
|
||||
self.kind().is_eager()
|
||||
}
|
||||
|
||||
pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId {
|
||||
self.reachability
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -348,24 +348,21 @@ impl<'db> UseDefMap<'db> {
|
|||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
/// Returns true if a given 'use' of a symbol is reachable from the start of the scope.
|
||||
/// For example, in the following code, use `2` is reachable, but `1` and `3` are not:
|
||||
/// ```py
|
||||
/// def f():
|
||||
/// x = 1
|
||||
/// if False:
|
||||
/// x # 1
|
||||
/// x # 2
|
||||
/// return
|
||||
/// x # 3
|
||||
/// ```
|
||||
pub(crate) fn is_symbol_use_reachable(&self, db: &dyn crate::Db, use_id: ScopedUseId) -> bool {
|
||||
pub(super) fn is_reachable(
|
||||
&self,
|
||||
db: &dyn crate::Db,
|
||||
reachability: ScopedVisibilityConstraintId,
|
||||
) -> bool {
|
||||
!self
|
||||
.visibility_constraints
|
||||
.evaluate(db, &self.predicates, self.reachability_by_use[use_id])
|
||||
.evaluate(db, &self.predicates, reachability)
|
||||
.is_always_false()
|
||||
}
|
||||
|
||||
pub(super) fn is_symbol_use_reachable(&self, db: &dyn crate::Db, use_id: ScopedUseId) -> bool {
|
||||
self.is_reachable(db, self.reachability_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn public_bindings(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
|
@ -618,7 +615,7 @@ pub(super) struct UseDefMapBuilder<'db> {
|
|||
/// ```
|
||||
/// Depending on the value of `test`, the `y = 1`, `y = 2`, or both bindings may be visible.
|
||||
/// The use of `x` is recorded with a reachability constraint of `[test]`.
|
||||
reachability: ScopedVisibilityConstraintId,
|
||||
pub(super) reachability: ScopedVisibilityConstraintId,
|
||||
|
||||
/// Tracks whether or not a given use of a symbol is reachable from the start of the scope.
|
||||
reachability_by_use: IndexVec<ScopedUseId, ScopedVisibilityConstraintId>,
|
||||
|
|
|
@ -4302,7 +4302,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} else {
|
||||
let use_id = name_node.scoped_use_id(db, scope);
|
||||
let symbol = symbol_from_bindings(db, use_def.bindings_at_use(use_id));
|
||||
let report_unresolved_usage = use_def.is_symbol_use_reachable(db, use_id);
|
||||
let report_unresolved_usage =
|
||||
self.index
|
||||
.is_symbol_use_reachable(db, file_scope_id, use_id);
|
||||
(symbol, report_unresolved_usage)
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue