mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Preserve narrowing after guarded match subject captures (#29558)
When a match case captures the subject under its own name and its guard fails, later cases now narrow that capture. Previously, those cases could retain a wider type because narrowing targeted only the binding read before the match. Retain bindings that capture the entire subject under its original name, including `as` patterns and OR alternatives. Captures of extracted values and assignments made by guards remain distinct from the saved subject. Fixes astral-sh/ty#4761.
This commit is contained in:
1 parent
3c8166b683
commit
95888b2edf
2 files changed
+144
No files matched your search
@@ -2887,6 +2887,42 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
(predicate, predicate_id)
|
||||
}
|
||||
|
||||
/// Retain captures that bind the entire subject to its original name. These definitions
|
||||
/// still refer to the saved subject if a guard fails, so later cases can narrow them.
|
||||
fn extend_match_subject_bindings(
|
||||
&self,
|
||||
pattern: &ast::Pattern,
|
||||
symbol: ScopedSymbolId,
|
||||
bindings: &mut SmallVec<[ScopedDefinitionId; 2]>,
|
||||
) {
|
||||
match pattern {
|
||||
ast::Pattern::MatchAs(pattern) => {
|
||||
if let Some(name) = &pattern.name
|
||||
&& let Some(definition) = self
|
||||
.definitions_by_node
|
||||
.get(&name.into())
|
||||
.and_then(|definitions| definitions.first())
|
||||
{
|
||||
let use_def = self.current_use_def_map();
|
||||
bindings.extend(use_def.symbol_binding_definition_ids(symbol).filter(
|
||||
|binding| use_def.definition(*binding).definition() == Some(*definition),
|
||||
));
|
||||
}
|
||||
if let Some(pattern) = &pattern.pattern {
|
||||
self.extend_match_subject_bindings(pattern, symbol, bindings);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchOr(pattern) => {
|
||||
for pattern in &pattern.patterns {
|
||||
self.extend_match_subject_bindings(pattern, symbol, bindings);
|
||||
}
|
||||
}
|
||||
// Structural patterns can capture extracted values. Identifying whole-subject
|
||||
// captures in class patterns requires type information.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
|
||||
/// standalone (type narrowing tests, RHS of an assignment.)
|
||||
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
|
||||
@@ -4922,6 +4958,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
};
|
||||
subject_targets.push((place, bindings));
|
||||
}
|
||||
let subject_place = PlaceExpr::try_from_expr(subject.as_ref())
|
||||
.and_then(|place| self.current_place_table().place_id((&place).into()));
|
||||
let places = self.current_place_table();
|
||||
let ast_ids = self.current_ast_ids();
|
||||
let mut sequence_subject_targets =
|
||||
@@ -4974,6 +5012,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.record_exception_checkpoint_if(Self::pattern_can_raise(&case.pattern));
|
||||
self.visit_pattern(&case.pattern);
|
||||
self.current_match_case = None;
|
||||
if let Some((ScopedPlaceId::Symbol(symbol), bindings)) = subject_targets
|
||||
.iter_mut()
|
||||
.find(|(place, _)| Some(*place) == subject_place)
|
||||
{
|
||||
self.extend_match_subject_bindings(&case.pattern, *symbol, bindings);
|
||||
}
|
||||
// unlike in [Stmt::If], we don't reset [no_case_matched]
|
||||
// here because the effects of visiting a pattern is binding
|
||||
// symbols, and this doesn't occur unless the pattern
|
||||
|
||||
@@ -3545,6 +3545,95 @@ def match_tuple_expression_guard_rebinding(
|
||||
reveal_type(b) # revealed: TupleSubjectB1
|
||||
```
|
||||
|
||||
## Captures of the subject across cases
|
||||
|
||||
### Narrowing guards
|
||||
|
||||
Capturing the subject under its original name keeps its value. If the guard fails, later patterns
|
||||
can still narrow that capture.
|
||||
|
||||
```py
|
||||
def narrow_after_capture(value: int | str | bytes) -> None:
|
||||
match value:
|
||||
case value if isinstance(value, int):
|
||||
reveal_type(value) # revealed: int
|
||||
case str():
|
||||
reveal_type(value) # revealed: str
|
||||
case bytes():
|
||||
reveal_type(value) # revealed: bytes
|
||||
```
|
||||
|
||||
### Constrained `as` patterns
|
||||
|
||||
If the first pattern matches but its guard fails, the captured integer cannot reach the string case.
|
||||
|
||||
```py
|
||||
def narrow_after_as_capture(value: int | str, flag: bool) -> None:
|
||||
match value:
|
||||
case int() as value if flag:
|
||||
pass
|
||||
case str():
|
||||
reveal_type(value) # revealed: str
|
||||
```
|
||||
|
||||
### Or pattern alternatives
|
||||
|
||||
Each alternative can capture the whole subject under its original name.
|
||||
|
||||
```py
|
||||
def narrow_after_or_capture(value: int | str | bytes, flag: bool) -> None:
|
||||
match value:
|
||||
case (int() as value) | (str() as value) if flag:
|
||||
pass
|
||||
case str():
|
||||
reveal_type(value) # revealed: str
|
||||
```
|
||||
|
||||
A capture can also refer to the whole subject in one alternative and an extracted element in
|
||||
another. Matching the original tuple in a later case does not make its captured integer unreachable.
|
||||
|
||||
```py
|
||||
def mixed_or_captures(subject: int | tuple[int], flag: bool) -> None:
|
||||
value = subject
|
||||
match value:
|
||||
case (int() as value) | [value] if flag:
|
||||
pass
|
||||
case tuple():
|
||||
reveal_type(value) # revealed: int
|
||||
```
|
||||
|
||||
### Reassignment in guards
|
||||
|
||||
A guard can assign a different value to the captured name. Later cases still match the saved
|
||||
subject, so their patterns do not narrow that assignment.
|
||||
|
||||
```py
|
||||
def reassign_capture(value: int | str, flag: bool) -> None:
|
||||
match value:
|
||||
case value if (value := "changed") and flag:
|
||||
pass
|
||||
case int():
|
||||
reveal_type(value) # revealed: Literal["changed"]
|
||||
```
|
||||
|
||||
### Member subjects
|
||||
|
||||
Capturing an attribute under the name of its owner replaces the owner with the attribute value.
|
||||
Narrowing the saved attribute does not narrow this new binding of the owner's name.
|
||||
|
||||
```py
|
||||
class Holder:
|
||||
attr: int | str
|
||||
|
||||
def capture_member(holder: Holder) -> None:
|
||||
value = holder
|
||||
match value.attr:
|
||||
case value if False:
|
||||
pass
|
||||
case int():
|
||||
reveal_type(value) # revealed: int | str
|
||||
```
|
||||
|
||||
## Named-expression subjects
|
||||
|
||||
A named expression creates a new binding for the subject. The successful pattern narrows that
|
||||
@@ -3569,6 +3658,17 @@ def match_named_expression_subject_capture(value: tuple[int]) -> None:
|
||||
reveal_type(subject) # revealed: int
|
||||
```
|
||||
|
||||
Capturing the entire subject under its new name preserves narrowing in later cases.
|
||||
|
||||
```py
|
||||
def match_named_expression_guarded_capture(value: int | str) -> None:
|
||||
match subject := value:
|
||||
case subject if False:
|
||||
pass
|
||||
case int():
|
||||
reveal_type(subject) # revealed: int
|
||||
```
|
||||
|
||||
## Cycles in pattern binding types
|
||||
|
||||
Pattern captures can affect the type of a later match subject, including through a loop or a
|
||||
|
||||
Reference in new issue
Block a user