[ty] make del x force local resolution of x in the current scope (#19389)

Fixes https://github.com/astral-sh/ty/issues/769.

**Updated:** The preferred approach here is to keep the SemanticIndex
simple (`del` of any name marks that name "bound" in the current scope)
and to move complexity to type inference (free variable resolution stops
when it finds a binding, unless that binding is declared `nonlocal`). As
part of this change, free variable resolution will now union the types
it finds as it walks in enclosing scopes. This approach is still
incomplete, because it doesn't consider inner scopes or sibling scopes,
but it improves the common case.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Jack O'Connor 2025-07-18 14:58:32 -07:00 committed by GitHub
parent 360eb7005f
commit e9a64e5825
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 187 additions and 18 deletions

View file

@ -1994,8 +1994,26 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
walk_stmt(self, stmt);
for target in targets {
if let Ok(target) = PlaceExpr::try_from(target) {
let is_name = target.is_name();
let place_id = self.add_place(PlaceExprWithFlags::new(target));
self.current_place_table_mut().mark_place_used(place_id);
let place_table = self.current_place_table_mut();
if is_name {
// `del x` behaves like an assignment in that it forces all references
// to `x` in the current scope (including *prior* references) to refer
// to the current scope's binding (unless `x` is declared `global` or
// `nonlocal`). For example, this is an UnboundLocalError at runtime:
//
// ```py
// x = 1
// def foo():
// print(x) # can't refer to global `x`
// if False:
// del x
// foo()
// ```
place_table.mark_place_bound(place_id);
}
place_table.mark_place_used(place_id);
self.delete_binding(place_id);
}
}