mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[ty] Fix binary intersection comparison inference logic (#18266)
## Summary Resolves https://github.com/astral-sh/ty/issues/485. `infer_binary_intersection_type_comparison()` now checks for all positive members before concluding that an operation is unsupported for a given intersection type. ## Test Plan Markdown tests. --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
6392dccd24
commit
a1399656c9
2 changed files with 95 additions and 25 deletions
|
@ -109,23 +109,50 @@ def _(o: object):
|
|||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
Raise an error if the given operator is unsupported for all positive contributions to the
|
||||
intersection type:
|
||||
|
||||
```py
|
||||
class NonContainer1: ...
|
||||
class NonContainer2: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, NonContainer1):
|
||||
if isinstance(x, NonContainer2):
|
||||
reveal_type(x) # revealed: NonContainer1 & NonContainer2
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer1`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
Do not raise an error if at least one of the positive contributions to the intersection type support
|
||||
the operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool:
|
||||
return False
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
if isinstance(x, NonContainer1):
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer2):
|
||||
reveal_type(x) # revealed: NonContainer1 & Container & NonContainer2
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
Do also raise an error if the intersection has no positive contributions at all, unless the operator
|
||||
is supported on `object`:
|
||||
|
||||
```py
|
||||
def _(x: object):
|
||||
if not isinstance(x, NonContainer1):
|
||||
reveal_type(x) # revealed: ~NonContainer1
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `object`, in comparing `Literal[2]` with `~NonContainer1`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
|
||||
reveal_type(2 is x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
|
|
@ -6537,20 +6537,27 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
intersection_on: IntersectionOn,
|
||||
range: TextRange,
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
enum State<'db> {
|
||||
// We have not seen any positive elements (yet)
|
||||
NoPositiveElements,
|
||||
// The operator was unsupported on all elements that we have seen so far.
|
||||
// Contains the first error we encountered.
|
||||
UnsupportedOnAllElements(CompareUnsupportedError<'db>),
|
||||
// The operator was supported on at least one positive element.
|
||||
Supported,
|
||||
}
|
||||
|
||||
// If a comparison yields a definitive true/false answer on a (positive) part
|
||||
// of an intersection type, it will also yield a definitive answer on the full
|
||||
// intersection type, which is even more specific.
|
||||
for pos in intersection.positive(self.db()) {
|
||||
let result = match intersection_on {
|
||||
IntersectionOn::Left => {
|
||||
self.infer_binary_type_comparison(*pos, op, other, range)?
|
||||
}
|
||||
IntersectionOn::Right => {
|
||||
self.infer_binary_type_comparison(other, op, *pos, range)?
|
||||
}
|
||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other, range),
|
||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos, range),
|
||||
};
|
||||
if let Type::BooleanLiteral(b) = result {
|
||||
return Ok(Type::BooleanLiteral(b));
|
||||
|
||||
if let Ok(Type::BooleanLiteral(_)) = result {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6619,19 +6626,55 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
builder = builder.add_positive(KnownClass::Bool.to_instance(self.db()));
|
||||
|
||||
let mut state = State::NoPositiveElements;
|
||||
|
||||
for pos in intersection.positive(self.db()) {
|
||||
let result = match intersection_on {
|
||||
IntersectionOn::Left => {
|
||||
self.infer_binary_type_comparison(*pos, op, other, range)?
|
||||
}
|
||||
IntersectionOn::Right => {
|
||||
self.infer_binary_type_comparison(other, op, *pos, range)?
|
||||
}
|
||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other, range),
|
||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos, range),
|
||||
};
|
||||
builder = builder.add_positive(result);
|
||||
|
||||
match result {
|
||||
Ok(ty) => {
|
||||
state = State::Supported;
|
||||
builder = builder.add_positive(ty);
|
||||
}
|
||||
Err(error) => {
|
||||
match state {
|
||||
State::NoPositiveElements => {
|
||||
// This is the first positive element, but the operation is not supported.
|
||||
// Store the error and continue.
|
||||
state = State::UnsupportedOnAllElements(error);
|
||||
}
|
||||
State::UnsupportedOnAllElements(_) => {
|
||||
// We already have an error stored, and continue to see elements on which
|
||||
// the operator is not supported. Continue with the same state (only keep
|
||||
// the first error).
|
||||
}
|
||||
State::Supported => {
|
||||
// We previously saw a positive element that supported the operator,
|
||||
// so the overall operation is still supported.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(builder.build())
|
||||
match state {
|
||||
State::Supported => Ok(builder.build()),
|
||||
State::NoPositiveElements => {
|
||||
// We didn't see any positive elements, check if the operation is supported on `object`:
|
||||
match intersection_on {
|
||||
IntersectionOn::Left => {
|
||||
self.infer_binary_type_comparison(Type::object(self.db()), op, other, range)
|
||||
}
|
||||
IntersectionOn::Right => {
|
||||
self.infer_binary_type_comparison(other, op, Type::object(self.db()), range)
|
||||
}
|
||||
}
|
||||
}
|
||||
State::UnsupportedOnAllElements(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Infers the type of a binary comparison (e.g. 'left == right'). See
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue