Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
[0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3]

Expand All @@ -34,4 +28,3 @@ def name_3():
@name_3
async def name_5():
pass
```
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,23 @@ class Foo:
class Baz[T: Foo]:
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]
type S = a
type T = b
type U = Foo
# error: [unresolved-reference]
type V = Bar
type W = Baz

def h[T: Bar]():
# error: [unresolved-reference]
Expand Down Expand Up @@ -141,9 +155,23 @@ class Foo:
class Baz[T: Foo]:
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]
type S = a
type T = b
type U = Foo
# error: [unresolved-reference]
type V = Bar
type W = Baz

def h[T: Bar]():
# error: [unresolved-reference]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ Iterating over an unbound iterable yields `Unknown`:
# error: [not-iterable] "Object of type `int` is not iterable"
# revealed: tuple[int, Unknown]
[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
Expand Down
37 changes: 37 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,43 @@ class C[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

Just like regular symbols, the typevars of a generic class are only available in that class's scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ d.x = 1
reveal_type(d.x) # revealed: Literal[1]
d.x = 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:
Expand Down
6 changes: 6 additions & 0 deletions crates/ty_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {

// Records snapshots of the place states visible from the current eager scope.
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
// which bindings reach each of the uses in the scope. Loop through each enclosing scope,
// looking for any that bind each place.
Expand All @@ -328,6 +331,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
// ```
for enclosing_scope_info in self.scope_stack.iter().rev() {
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_place_table = &self.place_tables[enclosing_scope_id];

Expand Down Expand Up @@ -355,6 +359,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
enclosing_place_id,
enclosing_scope_kind,
enclosing_place,
popped_scope_is_annotation_scope && is_immediately_enclosing_scope,
);
self.enclosing_snapshots.insert(key, eager_snapshot);
}
Expand Down Expand Up @@ -429,6 +434,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
enclosed_symbol_id.into(),
enclosing_scope_kind,
enclosing_place.into(),
false,
);
self.enclosing_snapshots.insert(key, lazy_snapshot);
}
Expand Down
12 changes: 8 additions & 4 deletions crates/ty_python_semantic/src/semantic_index/use_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,17 +1186,21 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn snapshot_enclosing_state(
&mut self,
enclosing_place: ScopedPlaceId,
scope: ScopeKind,
enclosing_scope: ScopeKind,
enclosing_place_expr: PlaceExprRef,
is_parent_of_annotation_scope: bool,
) -> ScopedEnclosingSnapshotId {
let bindings = match enclosing_place {
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].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),
// so we never need to save eager scope bindings in a class scope.
if (scope.is_class() && enclosing_place.is_symbol()) || !enclosing_place_expr.is_bound() {
let is_class_symbol = enclosing_scope.is_class() && enclosing_place.is_symbol();
// Names bound in class scopes are never visible to nested scopes (but
// 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(
bindings.unbound_narrowing_constraint(),
))
Expand Down
37 changes: 27 additions & 10 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8319,6 +8319,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut nonlocal_union_builder = UnionBuilder::new(db);
let mut found_some_definition = false;
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
// scope differently (because an unbound name there falls back to builtins), so
// check only function-like scopes.
Expand Down Expand Up @@ -8349,6 +8355,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// registering eager bindings for nested scopes that are actually eager, and for
// enclosing scopes that actually contain bindings that we should use when
// resolving the reference.)
let mut eagerly_resolved_place = None;
if !self.is_deferred() {
match self.index.enclosing_snapshot(
enclosing_scope_file_id,
Expand All @@ -8360,6 +8367,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
enclosing_scope_file_id,
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) => {
let place = place_from_bindings(db, bindings).map_type(|ty| {
Expand Down Expand Up @@ -8421,18 +8433,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// `nonlocal` variable, but we don't enforce that here. See the
// `ast::Stmt::AnnAssign` handling in `SemanticIndexBuilder::visit_stmt`.)
if enclosing_place.is_bound() || enclosing_place.is_declared() {
let local_place_and_qualifiers = place(
db,
enclosing_scope_id,
place_expr,
ConsideredDefinitions::AllReachable,
)
.map_type(|ty| {
self.narrow_place_with_applicable_constraints(
let local_place_and_qualifiers = eagerly_resolved_place.unwrap_or_else(|| {
place(
db,
enclosing_scope_id,
place_expr,
ty,
&constraint_keys,
ConsideredDefinitions::AllReachable,
)
.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
// this scope contains a `del` statement but no binding or declaration.
Expand Down Expand Up @@ -8475,6 +8489,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
FileScopeId::global(),
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) => {
let place = place_from_bindings(db, bindings).map_type(|ty| {
Expand Down
Loading