Skip to content

Commit a1eaa77

Browse files
committed
[ty] Make special cases for UnionType slightly narrower
1 parent f790444 commit a1eaa77

File tree

3 files changed

+153
-39
lines changed

3 files changed

+153
-39
lines changed

crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,15 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
135135
None | None # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`"
136136
```
137137

138-
When constructing something non-sensical like `int | 1`, we could ideally emit a diagnostic for the
139-
expression itself, as it leads to a `TypeError` at runtime. No other type checker supports this, so
140-
for now we only emit an error when it is used in a type expression:
138+
When constructing something nonsensical like `int | 1`, we emit a diagnostic for the expression
139+
itself, as it leads to a `TypeError` at runtime. The result of the expression is then inferred as
140+
`Unknown`, so we permit it to be used in a type expression.
141141

142142
```py
143-
IntOrOne = int | 1
143+
IntOrOne = int | 1 # error: [unsupported-operator]
144+
145+
reveal_type(IntOrOne) # revealed: Unknown
144146

145-
# error: [invalid-type-form] "Variable of type `Literal[1]` is not allowed in a type expression"
146147
def _(int_or_one: IntOrOne):
147148
reveal_type(int_or_one) # revealed: Unknown
148149
```
@@ -160,6 +161,70 @@ def f(SomeUnionType: UnionType):
160161
f(int | str)
161162
```
162163

164+
## `|` operator between class objects and non-class objects
165+
166+
Using the `|` operator between a class object and a non-class object does not create a `UnionType`
167+
instance; it calls the relevant dunder as normal:
168+
169+
```py
170+
class Foo:
171+
def __or__(self, other) -> str:
172+
return "foo"
173+
174+
reveal_type(Foo() | int) # revealed: str
175+
176+
class Bar:
177+
def __ror__(self, other) -> str:
178+
return "bar"
179+
180+
reveal_type(int | Bar()) # revealed: str
181+
182+
class Invalid:
183+
def __or__(self, other: "Invalid") -> str:
184+
return "Invalid"
185+
186+
# error: [unsupported-operator]
187+
reveal_type(int | Invalid()) # revealed: Unknown
188+
```
189+
190+
## Custom `__(r)or__` methods on metaclasses are only partially respected
191+
192+
A drawback of our extensive special casing of `|` operations between class objects is that
193+
`__(r)or__` methods on metaclasses are completely disregarded if two classes are `|`'d together. We
194+
respect the metaclass dunder if a class is `|`'d with a non-class, however:
195+
196+
```py
197+
class Meta(type):
198+
def __or__(self, other) -> str:
199+
return "Meta"
200+
201+
class Foo(metaclass=Meta): ...
202+
class Bar(metaclass=Meta): ...
203+
204+
X = Foo | Bar
205+
206+
# In an ideal world, perhaps we would respect `Meta.__or__` here and reveal `str`?
207+
# But we still need to record what the elements are, since (according to the typing spec)
208+
# `X` is still a valid type alias
209+
reveal_type(X) # revealed: types.UnionType
210+
211+
def f(obj: X):
212+
reveal_type(obj) # revealed: Foo | Bar
213+
214+
# We do respect the metaclass `__or__` if it's used between a class and a non-class, however:
215+
216+
Y = Foo | 42
217+
reveal_type(Y) # revealed: str
218+
219+
Z = Bar | 56
220+
reveal_type(Z) # revealed: str
221+
222+
def g(
223+
arg1: Y, # error: [invalid-type-form]
224+
arg2: Z, # error: [invalid-type-form]
225+
): ...
226+
```
227+
163228
## Generic types
164229

165230
Implicit type aliases can also refer to generic types:
@@ -191,7 +256,8 @@ From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/
191256
> type hint is acceptable in a type alias
192257
193258
However, no other type checker seems to support stringified annotations in implicit type aliases. We
194-
currently also do not support them:
259+
currently also do not support them, and we detect places where these attempted unions cause runtime
260+
errors:
195261

196262
```py
197263
AliasForStr = "str"
@@ -200,9 +266,10 @@ AliasForStr = "str"
200266
def _(s: AliasForStr):
201267
reveal_type(s) # revealed: Unknown
202268

203-
IntOrStr = int | "str"
269+
IntOrStr = int | "str" # error: [unsupported-operator]
270+
271+
reveal_type(IntOrStr) # revealed: Unknown
204272

205-
# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
206273
def _(int_or_str: IntOrStr):
207274
reveal_type(int_or_str) # revealed: Unknown
208275
```

crates/ty_python_semantic/src/types/call.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::context::InferContext;
22
use super::{Signature, Type, TypeContext};
33
use crate::Db;
4-
use crate::types::PropertyInstanceType;
54
use crate::types::call::bind::BindingError;
5+
use crate::types::{MemberLookupPolicy, PropertyInstanceType};
66
use ruff_python_ast as ast;
77

88
mod arguments;
@@ -16,6 +16,16 @@ impl<'db> Type<'db> {
1616
left_ty: Type<'db>,
1717
op: ast::Operator,
1818
right_ty: Type<'db>,
19+
) -> Result<Bindings<'db>, CallBinOpError> {
20+
Self::try_call_bin_op_with_policy(db, left_ty, op, right_ty, MemberLookupPolicy::default())
21+
}
22+
23+
pub(crate) fn try_call_bin_op_with_policy(
24+
db: &'db dyn Db,
25+
left_ty: Type<'db>,
26+
op: ast::Operator,
27+
right_ty: Type<'db>,
28+
policy: MemberLookupPolicy,
1929
) -> Result<Bindings<'db>, CallBinOpError> {
2030
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
2131
// the Python spec [1] is:
@@ -43,39 +53,43 @@ impl<'db> Type<'db> {
4353
&& rhs_reflected != left_class.member(db, reflected_dunder).place
4454
{
4555
return Ok(right_ty
46-
.try_call_dunder(
56+
.try_call_dunder_with_policy(
4757
db,
4858
reflected_dunder,
49-
CallArguments::positional([left_ty]),
59+
&mut CallArguments::positional([left_ty]),
5060
TypeContext::default(),
61+
policy,
5162
)
5263
.or_else(|_| {
53-
left_ty.try_call_dunder(
64+
left_ty.try_call_dunder_with_policy(
5465
db,
5566
op.dunder(),
56-
CallArguments::positional([right_ty]),
67+
&mut CallArguments::positional([right_ty]),
5768
TypeContext::default(),
69+
policy,
5870
)
5971
})?);
6072
}
6173
}
6274

63-
let call_on_left_instance = left_ty.try_call_dunder(
75+
let call_on_left_instance = left_ty.try_call_dunder_with_policy(
6476
db,
6577
op.dunder(),
66-
CallArguments::positional([right_ty]),
78+
&mut CallArguments::positional([right_ty]),
6779
TypeContext::default(),
80+
policy,
6881
);
6982

7083
call_on_left_instance.or_else(|_| {
7184
if left_ty == right_ty {
7285
Err(CallBinOpError::NotSupported)
7386
} else {
74-
Ok(right_ty.try_call_dunder(
87+
Ok(right_ty.try_call_dunder_with_policy(
7588
db,
7689
op.reflected_dunder(),
77-
CallArguments::positional([left_ty]),
90+
&mut CallArguments::positional([left_ty]),
7891
TypeContext::default(),
92+
policy,
7993
)?)
8094
}
8195
})

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8474,42 +8474,75 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
84748474
| Type::GenericAlias(..)
84758475
| Type::SpecialForm(_)
84768476
| Type::KnownInstance(KnownInstanceType::UnionType(_)),
8477-
_,
8478-
ast::Operator::BitOr,
8479-
)
8480-
| (
8481-
_,
84828477
Type::ClassLiteral(..)
84838478
| Type::SubclassOf(..)
84848479
| Type::GenericAlias(..)
84858480
| Type::SpecialForm(_)
84868481
| Type::KnownInstance(KnownInstanceType::UnionType(_)),
84878482
ast::Operator::BitOr,
84888483
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => {
8489-
// For a value expression like `int | None`, the inferred type for `None` will be
8490-
// a nominal instance of `NoneType`, so we need to convert it to a class literal
8491-
// such that it can later be converted back to a nominal instance type when calling
8492-
// `.in_type_expression` on the `UnionType` instance.
8493-
let convert_none_type = |ty: Type<'db>| {
8494-
if ty.is_none(self.db()) {
8495-
KnownClass::NoneType.to_class_literal(self.db())
8496-
} else {
8497-
ty
8498-
}
8499-
};
8500-
85018484
if left_ty.is_equivalent_to(self.db(), right_ty) {
85028485
Some(left_ty)
85038486
} else {
85048487
Some(Type::KnownInstance(KnownInstanceType::UnionType(
8505-
UnionTypeInstance::new(
8506-
self.db(),
8507-
convert_none_type(left_ty),
8508-
convert_none_type(right_ty),
8509-
),
8488+
UnionTypeInstance::new(self.db(), left_ty, right_ty),
85108489
)))
85118490
}
85128491
}
8492+
(
8493+
not_none @ (Type::ClassLiteral(..)
8494+
| Type::SubclassOf(..)
8495+
| Type::GenericAlias(..)
8496+
| Type::KnownInstance(..)
8497+
| Type::SpecialForm(..)),
8498+
none @ Type::NominalInstance(instance),
8499+
ast::Operator::BitOr,
8500+
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310
8501+
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
8502+
{
8503+
Some(Type::KnownInstance(KnownInstanceType::UnionType(
8504+
UnionTypeInstance::new(self.db(), not_none, none),
8505+
)))
8506+
}
8507+
(
8508+
none @ Type::NominalInstance(instance),
8509+
not_none @ (Type::ClassLiteral(..)
8510+
| Type::SubclassOf(..)
8511+
| Type::GenericAlias(..)
8512+
| Type::KnownInstance(..)
8513+
| Type::SpecialForm(..)),
8514+
ast::Operator::BitOr,
8515+
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310
8516+
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
8517+
{
8518+
Some(Type::KnownInstance(KnownInstanceType::UnionType(
8519+
UnionTypeInstance::new(self.db(), none, not_none),
8520+
)))
8521+
}
8522+
8523+
// We avoid calling `type.__(r)or__`, as typeshed annotates these methods as
8524+
// accepting `Any` (since typeforms are inexpressable in the type system currently).
8525+
// This means that many common errors would not be caught if we fell back to typeshed's stubs here.
8526+
// Note that if a class had a custom metaclass that overrode `__(r)or__`, we would also ignore
8527+
// that custom method, but this seems like it's probably rare enough that it's acceptable.
8528+
(
8529+
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..),
8530+
_,
8531+
ast::Operator::BitOr,
8532+
)
8533+
| (
8534+
_,
8535+
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..),
8536+
ast::Operator::BitOr,
8537+
) => Type::try_call_bin_op_with_policy(
8538+
self.db(),
8539+
left_ty,
8540+
ast::Operator::BitOr,
8541+
right_ty,
8542+
MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
8543+
)
8544+
.ok()
8545+
.map(|binding| binding.return_type(self.db())),
85138546

85148547
// We've handled all of the special cases that we support for literals, so we need to
85158548
// fall back on looking for dunder methods on one of the operand types.

0 commit comments

Comments
 (0)