From f978f792b17393706c59eeae892c41f888727f75 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sat, 8 Nov 2025 00:15:23 +0900 Subject: [PATCH 1/9] [ty] fix a bug with global symbol lookup from eager scopes --- .../cyclic_symbol_in_comprehension.py} | 13 +++---------- .../resources/mdtest/comprehensions/basic.md | 18 ++++++++++++++++++ .../src/types/infer/builder.rs | 3 +++ 3 files changed, 24 insertions(+), 10 deletions(-) rename crates/ty_python_semantic/resources/{mdtest/regression/pr_20962_comprehension_panics.md => corpus/cyclic_symbol_in_comprehension.py} (56%) diff --git a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md b/crates/ty_python_semantic/resources/corpus/cyclic_symbol_in_comprehension.py similarity index 56% rename from crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md rename to crates/ty_python_semantic/resources/corpus/cyclic_symbol_in_comprehension.py index 97bbf21049363a..b7ba910e1e51f5 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md +++ b/crates/ty_python_semantic/resources/corpus/cyclic_symbol_in_comprehension.py @@ -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 . It -added two new fuzzer panics that are documented here for regression testing. - -## Too many cycle iterations in `place_by_id` - - - -```py name_5(name_3) [0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3] @@ -34,4 +28,3 @@ def name_3(): @name_3 async def name_5(): pass -``` diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index 5fac394404e576..0f2d1e4b0cf5b5 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3fe0bb003df581..0423c748685e5c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8475,6 +8475,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| { From fa052f64955157d424aa14f9f2ab9bd7f1788429 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sat, 8 Nov 2025 19:58:30 +0900 Subject: [PATCH 2/9] Update builder.rs --- crates/ty_python_semantic/src/types/infer/builder.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 0423c748685e5c..07e8bd00ff70c0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8471,10 +8471,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { file_scope_id, ) { EnclosingSnapshotResult::FoundConstraint(constraint) => { - constraint_keys.push(( + // The constraint should already have been added in the iteration over `ancestor_scopes` above. + debug_assert!(constraint_keys.contains(&( 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(); @@ -8487,10 +8488,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &constraint_keys, ) }); - constraint_keys.push(( + debug_assert!(constraint_keys.contains(&( FileScopeId::global(), ConstraintKey::NestedScope(file_scope_id), - )); + ))); return place.into(); } // There are no visible bindings / constraint here. From 300b87420f1c632801feb40ab87f85cf4aea8b77 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sun, 9 Nov 2025 16:50:32 +0900 Subject: [PATCH 3/9] Don't lookup global places in enclosing scope lookup --- .../resources/mdtest/generics/scoping.md | 25 +++++++++++++++++++ .../src/types/infer/builder.rs | 5 ++++ 2 files changed, 30 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 308092f4d12494..ae5616fe03026d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -288,6 +288,31 @@ 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): ... +``` + ## 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, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 07e8bd00ff70c0..b26520304ab8b0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8411,6 +8411,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if enclosing_place.as_symbol().is_some_and(Symbol::is_global) { break; } + // 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 == FileScopeId::global() && !found_some_definition { + break; + } let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file()); From 8eacf472f7692748b56314afa7f9dfc4d4ea441a Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sun, 9 Nov 2025 17:20:15 +0900 Subject: [PATCH 4/9] Update builder.rs --- .../src/types/infer/builder.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b26520304ab8b0..7beed3f38b6c90 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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 == FileScopeId::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. @@ -8411,11 +8417,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if enclosing_place.as_symbol().is_some_and(Symbol::is_global) { break; } - // 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 == FileScopeId::global() && !found_some_definition { - break; - } let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file()); @@ -8476,11 +8477,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { file_scope_id, ) { EnclosingSnapshotResult::FoundConstraint(constraint) => { - // The constraint should already have been added in the iteration over `ancestor_scopes` above. - debug_assert!(constraint_keys.contains(&( + constraint_keys.push(( 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(); @@ -8493,10 +8493,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &constraint_keys, ) }); - debug_assert!(constraint_keys.contains(&( + constraint_keys.push(( FileScopeId::global(), ConstraintKey::NestedScope(file_scope_id), - ))); + )); return place.into(); } // There are no visible bindings / constraint here. From 4c2d687af79de87e6d8ee61438b7233e52300aa8 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Mon, 10 Nov 2025 02:05:22 +0900 Subject: [PATCH 5/9] fix panics in nested scope cases --- .../resources/mdtest/generics/scoping.md | 12 ++++++++ .../src/types/infer/builder.rs | 28 ++++++++++++------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index ae5616fe03026d..79944b263aff3a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -311,6 +311,18 @@ 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 diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7beed3f38b6c90..22417498a57add 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8355,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, @@ -8366,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| { @@ -8427,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. From f2b0a25f7e996285a4197f31cba1283c4d1176fb Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:48:39 +0900 Subject: [PATCH 6/9] Update crates/ty_python_semantic/src/types/infer/builder.rs Co-authored-by: Micha Reiser --- crates/ty_python_semantic/src/types/infer/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 22417498a57add..53c1c1dc963cd1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8321,7 +8321,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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 == FileScopeId::global() { + if enclosing_scope_file_id.is_global() { break; } From 41ba670b23271896ef31bd137a58092d41a11de9 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Tue, 11 Nov 2025 11:12:43 +0900 Subject: [PATCH 7/9] Update complex_target.md --- .../resources/mdtest/narrow/complex_target.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md index 479238d617cea2..96b12d845d87f0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -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: From b62ff1021e38df1d1cdecd7e4e6536be37ff7efc Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Wed, 12 Nov 2025 12:50:55 +0900 Subject: [PATCH 8/9] fix snapshot behavior in annotation scopes annotation scopes can see names defined in an immediately-enclosing class scope --- .../resources/mdtest/annotations/deferred.md | 28 +++++++++++++++++++ .../src/semantic_index/builder.rs | 6 ++++ .../src/semantic_index/use_def.rs | 9 ++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md index 8db8d9040920a1..89b9324ea4ef25 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md @@ -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] @@ -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] diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 9352bf196c6278..6595ca508ba41e 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -314,6 +314,8 @@ 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]; + // 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. @@ -350,11 +352,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { nested_scope: popped_scope_id, nested_laziness: ScopeLaziness::Eager, }; + let is_immediately_enclosing_scope = popped_scope.kind().is_annotation() + && popped_scope.parent() == Some(enclosing_scope_id); let eager_snapshot = self.use_def_maps[enclosing_scope_id] .snapshot_enclosing_state( enclosing_place_id, enclosing_scope_kind, enclosing_place, + is_immediately_enclosing_scope, ); self.enclosing_snapshots.insert(key, eager_snapshot); } @@ -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); } diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index dcca102b873861..a1882ddb20ab94 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -1186,17 +1186,22 @@ 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_immediately_enclosing_scope: bool, ) -> ScopedEnclosingSnapshotId { let bindings = match enclosing_place { ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings(), ScopedPlaceId::Member(member) => self.member_states[member].bindings(), }; + 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. - if (scope.is_class() && enclosing_place.is_symbol()) || !enclosing_place_expr.is_bound() { + // There is one exception to this rule: annotation scopes can see + // names defined in an immediately-enclosing class scope. + if (is_class_symbol && !is_immediately_enclosing_scope) || !enclosing_place_expr.is_bound() + { self.enclosing_snapshots.push(EnclosingSnapshot::Constraint( bindings.unbound_narrowing_constraint(), )) From ab62b0bb6d5875636b733badb1a2e101f13e130e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 12 Nov 2025 08:06:22 -0800 Subject: [PATCH 9/9] minor adjustments --- .../src/semantic_index/builder.rs | 6 +++--- .../src/semantic_index/use_def.rs | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 6595ca508ba41e..cc1b1649fa7ef8 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -315,6 +315,7 @@ 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, @@ -330,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]; @@ -352,14 +354,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { nested_scope: popped_scope_id, nested_laziness: ScopeLaziness::Eager, }; - let is_immediately_enclosing_scope = popped_scope.kind().is_annotation() - && popped_scope.parent() == Some(enclosing_scope_id); let eager_snapshot = self.use_def_maps[enclosing_scope_id] .snapshot_enclosing_state( enclosing_place_id, enclosing_scope_kind, enclosing_place, - is_immediately_enclosing_scope, + popped_scope_is_annotation_scope && is_immediately_enclosing_scope, ); self.enclosing_snapshots.insert(key, eager_snapshot); } diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index a1882ddb20ab94..8ba5abce2bef8b 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -1188,7 +1188,7 @@ impl<'db> UseDefMapBuilder<'db> { enclosing_place: ScopedPlaceId, enclosing_scope: ScopeKind, enclosing_place_expr: PlaceExprRef, - is_immediately_enclosing_scope: bool, + is_parent_of_annotation_scope: bool, ) -> ScopedEnclosingSnapshotId { let bindings = match enclosing_place { ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings(), @@ -1196,12 +1196,11 @@ impl<'db> UseDefMapBuilder<'db> { }; 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_immediately_enclosing_scope) || !enclosing_place_expr.is_bound() - { + // 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(), ))