mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 03:36:18 +00:00
[ty] fix global symbol lookup from eager scopes (#21317)
## Summary
cf. https://github.com/astral-sh/ruff/pull/20962
In the following code, `foo` in the comprehension was not reported as
unresolved:
```python
# error: [unresolved-reference] "Name `foo` used when not defined"
foo
foo = [
# no error!
# revealed: Divergent
reveal_type(x) for _ in () for x in [foo]
]
baz = [
# error: [unresolved-reference] "Name `baz` used when not defined"
# revealed: Unknown
reveal_type(x) for _ in () for x in [baz]
]
```
In fact, this is a more serious bug than it looks: for `foo`,
[`explicit_global_symbol` is
called](6cc3393ccd/crates/ty_python_semantic/src/types/infer/builder.rs (L8052)),
causing a symbol that should actually be `Undefined` to be reported as
being of type `Divergent`.
This PR fixes this bug. As a result, the code in
`mdtest/regression/pr_20962_comprehension_panics.md` no longer panics.
## Test Plan
`corpus\cyclic_symbol_in_comprehension.py` is added.
New tests are added in `mdtest/comprehensions/basic.md`.
---------
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
a1d9cb5830
commit
9dd666d677
8 changed files with 136 additions and 24 deletions
|
|
@ -1,13 +1,7 @@
|
||||||
# Documentation of two fuzzer panics involving comprehensions
|
# Regression test for https://github.com/astral-sh/ruff/pull/20962
|
||||||
|
# error message:
|
||||||
|
# `place_by_id: execute: too many cycle iterations`
|
||||||
|
|
||||||
Type inference for comprehensions was added in <https://github.com/astral-sh/ruff/pull/20962>. It
|
|
||||||
added two new fuzzer panics that are documented here for regression testing.
|
|
||||||
|
|
||||||
## Too many cycle iterations in `place_by_id`
|
|
||||||
|
|
||||||
<!-- expect-panic: too many cycle iterations -->
|
|
||||||
|
|
||||||
```py
|
|
||||||
name_5(name_3)
|
name_5(name_3)
|
||||||
[0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3]
|
[0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3]
|
||||||
|
|
||||||
|
|
@ -34,4 +28,3 @@ else:
|
||||||
@name_3
|
@name_3
|
||||||
async def name_5():
|
async def name_5():
|
||||||
pass
|
pass
|
||||||
```
|
|
||||||
|
|
@ -87,9 +87,23 @@ class Foo:
|
||||||
class Baz[T: Foo]:
|
class Baz[T: Foo]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||||
|
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||||
|
class Qux(Foo, Bar, Baz):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||||
|
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||||
|
class Quux[_T](Foo, Bar, Baz):
|
||||||
|
pass
|
||||||
|
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
type S = a
|
type S = a
|
||||||
type T = b
|
type T = b
|
||||||
|
type U = Foo
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
type V = Bar
|
||||||
|
type W = Baz
|
||||||
|
|
||||||
def h[T: Bar]():
|
def h[T: Bar]():
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
|
|
@ -141,9 +155,23 @@ class Foo:
|
||||||
class Baz[T: Foo]:
|
class Baz[T: Foo]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||||
|
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||||
|
class Qux(Foo, Bar, Baz):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||||
|
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||||
|
class Quux[_T](Foo, Bar, Baz):
|
||||||
|
pass
|
||||||
|
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
type S = a
|
type S = a
|
||||||
type T = b
|
type T = b
|
||||||
|
type U = Foo
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
type V = Bar
|
||||||
|
type W = Baz
|
||||||
|
|
||||||
def h[T: Bar]():
|
def h[T: Bar]():
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,24 @@ Iterating over an unbound iterable yields `Unknown`:
|
||||||
# error: [not-iterable] "Object of type `int` is not iterable"
|
# error: [not-iterable] "Object of type `int` is not iterable"
|
||||||
# revealed: tuple[int, Unknown]
|
# revealed: tuple[int, Unknown]
|
||||||
[reveal_type((x, z)) for x in range(3) for z in x]
|
[reveal_type((x, z)) for x in range(3) for z in x]
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||||
|
foo
|
||||||
|
foo = [
|
||||||
|
# revealed: tuple[int, Unknown]
|
||||||
|
reveal_type((x, z))
|
||||||
|
for x in range(3)
|
||||||
|
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||||
|
for z in [foo]
|
||||||
|
]
|
||||||
|
|
||||||
|
baz = [
|
||||||
|
# revealed: tuple[int, Unknown]
|
||||||
|
reveal_type((x, z))
|
||||||
|
for x in range(3)
|
||||||
|
# error: [unresolved-reference] "Name `baz` used when not defined"
|
||||||
|
for z in [baz]
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Starred expressions
|
## Starred expressions
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,43 @@ class C[T]:
|
||||||
class Bad2(Iterable[T]): ...
|
class Bad2(Iterable[T]): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Class bases are evaluated within the type parameter scope
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[_T](
|
||||||
|
# error: [unresolved-reference] "Name `C` used when not defined"
|
||||||
|
C
|
||||||
|
): ...
|
||||||
|
|
||||||
|
# `D` in `list[D]` is resolved to be a type variable of class `D`.
|
||||||
|
class D[D](list[D]): ...
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `E` used when not defined"
|
||||||
|
if E:
|
||||||
|
class E[_T](
|
||||||
|
# error: [unresolved-reference] "Name `E` used when not defined"
|
||||||
|
E
|
||||||
|
): ...
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `F` used when not defined"
|
||||||
|
F
|
||||||
|
|
||||||
|
# error: [unresolved-reference] "Name `F` used when not defined"
|
||||||
|
class F[_T](F): ...
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
class G[_T](
|
||||||
|
# error: [unresolved-reference] "Name `G` used when not defined"
|
||||||
|
G
|
||||||
|
): ...
|
||||||
|
# error: [unresolved-reference] "Name `H` used when not defined"
|
||||||
|
if H:
|
||||||
|
class H[_T](
|
||||||
|
# error: [unresolved-reference] "Name `H` used when not defined"
|
||||||
|
H
|
||||||
|
): ...
|
||||||
|
```
|
||||||
|
|
||||||
## Class scopes do not cover inner scopes
|
## Class scopes do not cover inner scopes
|
||||||
|
|
||||||
Just like regular symbols, the typevars of a generic class are only available in that class's scope,
|
Just like regular symbols, the typevars of a generic class are only available in that class's scope,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,15 @@ d.x = 1
|
||||||
reveal_type(d.x) # revealed: Literal[1]
|
reveal_type(d.x) # revealed: Literal[1]
|
||||||
d.x = unknown()
|
d.x = unknown()
|
||||||
reveal_type(d.x) # revealed: Unknown
|
reveal_type(d.x) # revealed: Unknown
|
||||||
|
|
||||||
|
class E:
|
||||||
|
x: int | None = None
|
||||||
|
|
||||||
|
e = E()
|
||||||
|
|
||||||
|
if e.x is not None:
|
||||||
|
class _:
|
||||||
|
reveal_type(e.x) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
Narrowing can be "reset" by assigning to the attribute:
|
Narrowing can be "reset" by assigning to the attribute:
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
|
|
||||||
// Records snapshots of the place states visible from the current eager scope.
|
// Records snapshots of the place states visible from the current eager scope.
|
||||||
fn record_eager_snapshots(&mut self, popped_scope_id: FileScopeId) {
|
fn record_eager_snapshots(&mut self, popped_scope_id: FileScopeId) {
|
||||||
|
let popped_scope = &self.scopes[popped_scope_id];
|
||||||
|
let popped_scope_is_annotation_scope = popped_scope.kind().is_annotation();
|
||||||
|
|
||||||
// If the scope that we just popped off is an eager scope, we need to "lock" our view of
|
// If the scope that we just popped off is an eager scope, we need to "lock" our view of
|
||||||
// which bindings reach each of the uses in the scope. Loop through each enclosing scope,
|
// which bindings reach each of the uses in the scope. Loop through each enclosing scope,
|
||||||
// looking for any that bind each place.
|
// looking for any that bind each place.
|
||||||
|
|
@ -328,6 +331,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
// ```
|
// ```
|
||||||
for enclosing_scope_info in self.scope_stack.iter().rev() {
|
for enclosing_scope_info in self.scope_stack.iter().rev() {
|
||||||
let enclosing_scope_id = enclosing_scope_info.file_scope_id;
|
let enclosing_scope_id = enclosing_scope_info.file_scope_id;
|
||||||
|
let is_immediately_enclosing_scope = popped_scope.parent() == Some(enclosing_scope_id);
|
||||||
let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind();
|
let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind();
|
||||||
let enclosing_place_table = &self.place_tables[enclosing_scope_id];
|
let enclosing_place_table = &self.place_tables[enclosing_scope_id];
|
||||||
|
|
||||||
|
|
@ -355,6 +359,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
enclosing_place_id,
|
enclosing_place_id,
|
||||||
enclosing_scope_kind,
|
enclosing_scope_kind,
|
||||||
enclosing_place,
|
enclosing_place,
|
||||||
|
popped_scope_is_annotation_scope && is_immediately_enclosing_scope,
|
||||||
);
|
);
|
||||||
self.enclosing_snapshots.insert(key, eager_snapshot);
|
self.enclosing_snapshots.insert(key, eager_snapshot);
|
||||||
}
|
}
|
||||||
|
|
@ -429,6 +434,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
enclosed_symbol_id.into(),
|
enclosed_symbol_id.into(),
|
||||||
enclosing_scope_kind,
|
enclosing_scope_kind,
|
||||||
enclosing_place.into(),
|
enclosing_place.into(),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
self.enclosing_snapshots.insert(key, lazy_snapshot);
|
self.enclosing_snapshots.insert(key, lazy_snapshot);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1187,17 +1187,21 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||||
pub(super) fn snapshot_enclosing_state(
|
pub(super) fn snapshot_enclosing_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
enclosing_place: ScopedPlaceId,
|
enclosing_place: ScopedPlaceId,
|
||||||
scope: ScopeKind,
|
enclosing_scope: ScopeKind,
|
||||||
enclosing_place_expr: PlaceExprRef,
|
enclosing_place_expr: PlaceExprRef,
|
||||||
|
is_parent_of_annotation_scope: bool,
|
||||||
) -> ScopedEnclosingSnapshotId {
|
) -> ScopedEnclosingSnapshotId {
|
||||||
let bindings = match enclosing_place {
|
let bindings = match enclosing_place {
|
||||||
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings(),
|
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings(),
|
||||||
ScopedPlaceId::Member(member) => self.member_states[member].bindings(),
|
ScopedPlaceId::Member(member) => self.member_states[member].bindings(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Names bound in class scopes are never visible to nested scopes (but attributes/subscripts are visible),
|
let is_class_symbol = enclosing_scope.is_class() && enclosing_place.is_symbol();
|
||||||
// so we never need to save eager scope bindings in a class scope.
|
// Names bound in class scopes are never visible to nested scopes (but
|
||||||
if (scope.is_class() && enclosing_place.is_symbol()) || !enclosing_place_expr.is_bound() {
|
// attributes/subscripts are visible), so we never need to save eager scope bindings in a
|
||||||
|
// class scope. There is one exception to this rule: annotation scopes can see names
|
||||||
|
// defined in an immediately-enclosing class scope.
|
||||||
|
if (is_class_symbol && !is_parent_of_annotation_scope) || !enclosing_place_expr.is_bound() {
|
||||||
self.enclosing_snapshots.push(EnclosingSnapshot::Constraint(
|
self.enclosing_snapshots.push(EnclosingSnapshot::Constraint(
|
||||||
bindings.unbound_narrowing_constraint(),
|
bindings.unbound_narrowing_constraint(),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -8319,6 +8319,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
let mut nonlocal_union_builder = UnionBuilder::new(db);
|
let mut nonlocal_union_builder = UnionBuilder::new(db);
|
||||||
let mut found_some_definition = false;
|
let mut found_some_definition = false;
|
||||||
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) {
|
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) {
|
||||||
|
// If the current enclosing scope is global, no place lookup is performed here,
|
||||||
|
// instead falling back to the module's explicit global lookup below.
|
||||||
|
if enclosing_scope_file_id.is_global() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Class scopes are not visible to nested scopes, and we need to handle global
|
// Class scopes are not visible to nested scopes, and we need to handle global
|
||||||
// scope differently (because an unbound name there falls back to builtins), so
|
// scope differently (because an unbound name there falls back to builtins), so
|
||||||
// check only function-like scopes.
|
// check only function-like scopes.
|
||||||
|
|
@ -8349,6 +8355,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
// registering eager bindings for nested scopes that are actually eager, and for
|
// registering eager bindings for nested scopes that are actually eager, and for
|
||||||
// enclosing scopes that actually contain bindings that we should use when
|
// enclosing scopes that actually contain bindings that we should use when
|
||||||
// resolving the reference.)
|
// resolving the reference.)
|
||||||
|
let mut eagerly_resolved_place = None;
|
||||||
if !self.is_deferred() {
|
if !self.is_deferred() {
|
||||||
match self.index.enclosing_snapshot(
|
match self.index.enclosing_snapshot(
|
||||||
enclosing_scope_file_id,
|
enclosing_scope_file_id,
|
||||||
|
|
@ -8360,6 +8367,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
enclosing_scope_file_id,
|
enclosing_scope_file_id,
|
||||||
ConstraintKey::NarrowingConstraint(constraint),
|
ConstraintKey::NarrowingConstraint(constraint),
|
||||||
));
|
));
|
||||||
|
// If the current scope is eager, it is certain that the place is undefined in the current scope.
|
||||||
|
// Do not call the `place` query below as a fallback.
|
||||||
|
if scope.scope(db).is_eager() {
|
||||||
|
eagerly_resolved_place = Some(Place::Undefined.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
||||||
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
||||||
|
|
@ -8421,18 +8433,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
// `nonlocal` variable, but we don't enforce that here. See the
|
// `nonlocal` variable, but we don't enforce that here. See the
|
||||||
// `ast::Stmt::AnnAssign` handling in `SemanticIndexBuilder::visit_stmt`.)
|
// `ast::Stmt::AnnAssign` handling in `SemanticIndexBuilder::visit_stmt`.)
|
||||||
if enclosing_place.is_bound() || enclosing_place.is_declared() {
|
if enclosing_place.is_bound() || enclosing_place.is_declared() {
|
||||||
let local_place_and_qualifiers = place(
|
let local_place_and_qualifiers = eagerly_resolved_place.unwrap_or_else(|| {
|
||||||
db,
|
place(
|
||||||
enclosing_scope_id,
|
db,
|
||||||
place_expr,
|
enclosing_scope_id,
|
||||||
ConsideredDefinitions::AllReachable,
|
|
||||||
)
|
|
||||||
.map_type(|ty| {
|
|
||||||
self.narrow_place_with_applicable_constraints(
|
|
||||||
place_expr,
|
place_expr,
|
||||||
ty,
|
ConsideredDefinitions::AllReachable,
|
||||||
&constraint_keys,
|
|
||||||
)
|
)
|
||||||
|
.map_type(|ty| {
|
||||||
|
self.narrow_place_with_applicable_constraints(
|
||||||
|
place_expr,
|
||||||
|
ty,
|
||||||
|
&constraint_keys,
|
||||||
|
)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
// We could have `Place::Undefined` here, despite the checks above, for example if
|
// We could have `Place::Undefined` here, despite the checks above, for example if
|
||||||
// this scope contains a `del` statement but no binding or declaration.
|
// this scope contains a `del` statement but no binding or declaration.
|
||||||
|
|
@ -8475,6 +8489,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
FileScopeId::global(),
|
FileScopeId::global(),
|
||||||
ConstraintKey::NarrowingConstraint(constraint),
|
ConstraintKey::NarrowingConstraint(constraint),
|
||||||
));
|
));
|
||||||
|
// Reaching here means that no bindings are found in any scope.
|
||||||
|
// Since `explicit_global_symbol` may return a cycle initial value, we return `Place::Undefined` here.
|
||||||
|
return Place::Undefined.into();
|
||||||
}
|
}
|
||||||
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
||||||
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue