[ty] support del statement and deletion of except handler names (#18593)

## Summary

This PR closes https://github.com/astral-sh/ty/issues/238.

Since `DefinitionState::Deleted` was introduced in #18041, support for
the `del` statement (and deletion of except handler names) is
straightforward.

However, it is difficult to determine whether references to attributes
or subscripts are unresolved after they are deleted. This PR only
invalidates narrowing by assignment if the attribute or subscript is
deleted.

## Test Plan

`mdtest/del.md` is added.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Shunsuke Shibayama 2025-06-12 23:44:42 +09:00 committed by GitHub
parent 96171f41c2
commit ef564094a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 193 additions and 7 deletions

View file

@ -449,6 +449,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
}
}
fn delete_binding(&mut self, place: ScopedPlaceId) {
let is_place_name = self.current_place_table().place_expr(place).is_name();
self.current_use_def_map_mut()
.delete_binding(place, is_place_name);
}
/// Push a new [`Definition`] onto the list of definitions
/// associated with the `definition_node` AST node.
///
@ -1817,7 +1823,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = if let Some(symbol_name) = symbol_name {
let symbol = self.add_symbol(symbol_name.id.clone());
self.add_definition(
@ -1827,9 +1833,16 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
is_star: *is_star,
}),
);
}
Some(symbol)
} else {
None
};
self.visit_body(handler_body);
// The caught exception is cleared at the end of the except clause
if let Some(symbol) = symbol {
self.delete_binding(symbol);
}
// Each `except` block is mutually exclusive with all other `except` blocks.
post_except_states.push(self.flow_snapshot());
@ -1903,13 +1916,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
walk_stmt(self, stmt);
}
ast::Stmt::Delete(ast::StmtDelete { targets, range: _ }) => {
// We will check the target expressions and then delete them.
walk_stmt(self, stmt);
for target in targets {
if let Ok(target) = PlaceExpr::try_from(target) {
let place_id = self.add_place(target);
self.current_place_table().mark_place_used(place_id);
self.delete_binding(place_id);
}
}
walk_stmt(self, stmt);
}
ast::Stmt::Expr(ast::StmtExpr { value, range: _ }) if self.in_module_scope() => {
if let Some(expr) = dunder_all_extend_argument(value) {
@ -1956,7 +1971,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
}
(ast::ExprContext::Load, _) => (true, false),
(ast::ExprContext::Store, _) => (false, true),
(ast::ExprContext::Del, _) => (false, true),
(ast::ExprContext::Del, _) => (true, true),
(ast::ExprContext::Invalid, _) => (false, false),
};
let place_id = self.add_place(place_expr);

View file

@ -6145,7 +6145,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
match name.ctx {
ExprContext::Load => self.infer_name_load(name),
ExprContext::Store | ExprContext::Del => Type::Never,
ExprContext::Store => Type::Never,
ExprContext::Del => {
self.infer_name_load(name);
Type::Never
}
ExprContext::Invalid => Type::unknown(),
}
}
@ -6254,10 +6258,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match ctx {
ExprContext::Load => self.infer_attribute_load(attribute),
ExprContext::Store | ExprContext::Del => {
ExprContext::Store => {
self.infer_expression(value);
Type::Never
}
ExprContext::Del => {
self.infer_attribute_load(attribute);
Type::Never
}
ExprContext::Invalid => {
self.infer_expression(value);
Type::unknown()
@ -7646,12 +7654,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match ctx {
ExprContext::Load => self.infer_subscript_load(subscript),
ExprContext::Store | ExprContext::Del => {
ExprContext::Store => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
self.infer_subscript_expression_types(value, value_ty, slice_ty);
Type::Never
}
ExprContext::Del => {
self.infer_subscript_load(subscript);
Type::Never
}
ExprContext::Invalid => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);