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 74c1d42a49..ea674c8df3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -275,7 +275,49 @@ def f(a: A): a.x = None ``` -Narrowing is invalidated if a `nonlocal` declaration is made within a lazy scope. +The opposite is not true, that is, if a root expression is reassigned, narrowing on the member are +no longer valid in the inner lazy scope. + +```py +def f(l: list[str | None]): + if l[0] is not None: + def _(): + reveal_type(l[0]) # revealed: str | None + l = [None] + +def f(l: list[str | None]): + l[0] = "a" + def _(): + reveal_type(l[0]) # revealed: str | None + l = [None] + +def f(l: list[str | None]): + l[0] = "a" + def _(): + l: list[str | None] = [None] + def _(): + # TODO: should be `str | None` + reveal_type(l[0]) # revealed: Unknown + + def _(): + def _(): + reveal_type(l[0]) # revealed: str | None + l: list[str | None] = [None] + +def f(a: A): + if a.x is not None: + def _(): + reveal_type(a.x) # revealed: str | None + a = A() + +def f(a: A): + a.x = "a" + def _(): + reveal_type(a.x) # revealed: str | None + a = A() +``` + +Narrowing is also invalidated if a `nonlocal` declaration is made within a lazy scope. ```py def f(non_local: str | None): diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index d82605775a..036f079ef6 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6758,6 +6758,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .parent() .is_some_and(|parent| parent == enclosing_scope_file_id); + let has_root_place_been_reassigned = || { + let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); + enclosing_place_table + .parents(place_expr) + .any(|enclosing_root_place_id| { + enclosing_place_table + .place(enclosing_root_place_id) + .is_bound() + }) + }; + // If the reference is in a nested eager scope, we need to look for the place at // the point where the previous enclosing scope was defined, instead of at the end // of the scope. (Note that the semantic index builder takes care of only @@ -6799,28 +6810,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // There are no visible bindings / constraint here. // Don't fall back to non-eager place resolution. EnclosingSnapshotResult::NotFound => { - let enclosing_place_table = - self.index.place_table(enclosing_scope_file_id); - for enclosing_root_place_id in enclosing_place_table.parents(place_expr) - { - let enclosing_root_place = - enclosing_place_table.place(enclosing_root_place_id); - if enclosing_root_place.is_bound() { - if let Place::Type(_, _) = place( - db, - enclosing_scope_id, - enclosing_root_place, - ConsideredDefinitions::AllReachable, - ) - .place - { - return Place::Unbound.into(); - } - } + if has_root_place_been_reassigned() { + return Place::Unbound.into(); } continue; } - EnclosingSnapshotResult::NoLongerInEagerContext => {} + EnclosingSnapshotResult::NoLongerInEagerContext => { + if has_root_place_been_reassigned() { + return Place::Unbound.into(); + } + } } }