diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md index 3540e35b05..77307c5110 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -346,7 +346,10 @@ l: list[str | Literal[1] | None] = [None] def f(x: str | Literal[1] | None): class C: - if x is not None: # TODO: should be an unresolved-reference error + # If we try to access a variable in a class before it has been defined, + # the lookup will fall back to global. + # error: [unresolved-reference] + if x is not None: def _(): if x != 1: reveal_type(x) # revealed: str | None diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md index cf03d28680..b830e2ebe6 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -262,3 +262,28 @@ def f(): global __file__ # allowed, implicit global global int # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope" ``` + +## References to variables before they are defined within a class scope are considered global + +If we try to access a variable in a class before it has been defined, the lookup will fall back to +global. + +```py +x: str = "a" + +def f(x: int, y: int): + class C: + reveal_type(x) # revealed: int + + class D: + x = None + reveal_type(x) # revealed: None + + class E: + reveal_type(x) # revealed: str + x = None + + # error: [unresolved-reference] + reveal_type(y) # revealed: Unknown + y = None +``` diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 4b258bdd32..adc79dfe3b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6556,6 +6556,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(use_id) = use_id { constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); } + let local_is_unbound = local_scope_place.is_unbound(); let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || { let has_bindings_in_this_scope = match place_table.place_id(place_expr) { @@ -6574,7 +6575,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut is_nonlocal_binding = false; if let Some(symbol) = place_expr.as_symbol() { if let Some(symbol_id) = place_table.symbol_id(symbol.name()) { - if self.skip_non_global_scopes(file_scope_id, symbol_id) { + // If we try to access a variable in a class before it has been defined, + // the lookup will fall back to global. + let fallback_to_global = local_is_unbound + && has_bindings_in_this_scope + && scope.node(db).scope_kind().is_class(); + if self.skip_non_global_scopes(file_scope_id, symbol_id) || fallback_to_global { return global_symbol(self.db(), self.file(), symbol.name()).map_type( |ty| { self.narrow_place_with_applicable_constraints(