mirror of https://github.com/astral-sh/ruff
Use one inference
This commit is contained in:
parent
a424c97e2b
commit
074030d943
|
|
@ -2035,6 +2035,31 @@ def use_module(m: MyModule, param: int) -> None:
|
||||||
m.undefined_param = param
|
m.undefined_param = param
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `__setattr__` returning `Never` blocks all assignments
|
||||||
|
|
||||||
|
When `__setattr__` returns `Never` (indicating an immutable class), all attribute assignments are
|
||||||
|
blocked, even if the value type doesn't match `__setattr__`'s parameter type.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
class Immutable:
|
||||||
|
x: float
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: int) -> NoReturn:
|
||||||
|
raise AttributeError("Immutable")
|
||||||
|
|
||||||
|
def _(obj: Immutable) -> None:
|
||||||
|
# Even though `"foo"` doesn't match `__setattr__`'s `value: int` parameter,
|
||||||
|
# we still detect that `__setattr__` returns `Never` and block the assignment.
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `x` on type `Immutable` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||||
|
obj.x = "foo"
|
||||||
|
|
||||||
|
# Same for assignments that would match `__setattr__`'s parameter type.
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `x` on type `Immutable` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||||
|
obj.x = 42
|
||||||
|
```
|
||||||
|
|
||||||
## Objects of all types have a `__class__` method
|
## Objects of all types have a `__class__` method
|
||||||
|
|
||||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||||
|
|
|
||||||
|
|
@ -4578,29 +4578,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
// However, we would still have to perform the first inference without type context.
|
// However, we would still have to perform the first inference without type context.
|
||||||
let value_ty = infer_value_ty(self, TypeContext::default());
|
let value_ty = infer_value_ty(self, TypeContext::default());
|
||||||
|
|
||||||
|
// Infer `__setattr__` once upfront. We use this result for:
|
||||||
|
// 1. Checking if it returns `Never` (indicating an immutable class)
|
||||||
|
// 2. As a fallback when no explicit attribute is found
|
||||||
|
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
|
||||||
|
db,
|
||||||
|
"__setattr__",
|
||||||
|
&mut CallArguments::positional([Type::string_literal(db, attribute), value_ty]),
|
||||||
|
TypeContext::default(),
|
||||||
|
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if `__setattr__` returns `Never` (indicating an immutable class).
|
// Check if `__setattr__` returns `Never` (indicating an immutable class).
|
||||||
// If so, block all attribute assignments regardless of explicit attributes.
|
// If so, block all attribute assignments regardless of explicit attributes.
|
||||||
let setattr_returns_never = {
|
let setattr_returns_never = match &setattr_dunder_call_result {
|
||||||
let setattr_result = object_ty.try_call_dunder_with_policy(
|
Ok(result) => result.return_type(db).is_never(),
|
||||||
db,
|
Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()),
|
||||||
"__setattr__",
|
|
||||||
&mut CallArguments::positional([
|
|
||||||
Type::string_literal(db, attribute),
|
|
||||||
value_ty,
|
|
||||||
]),
|
|
||||||
TypeContext::default(),
|
|
||||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
|
||||||
);
|
|
||||||
match setattr_result {
|
|
||||||
Ok(result) => result.return_type(db).is_never(),
|
|
||||||
Err(CallDunderError::PossiblyUnbound(result)) => {
|
|
||||||
result.return_type(db).is_never()
|
|
||||||
}
|
|
||||||
// If __setattr__ rejects the type or doesn't exist, it doesn't return Never
|
|
||||||
Err(
|
|
||||||
CallDunderError::CallError(..) | CallDunderError::MethodNotAvailable,
|
|
||||||
) => false,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if setattr_returns_never {
|
if setattr_returns_never {
|
||||||
|
|
@ -4768,44 +4761,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
|
|
||||||
ensure_assignable_to(self, value_ty, instance_attr_ty)
|
ensure_assignable_to(self, value_ty, instance_attr_ty)
|
||||||
} else {
|
} else {
|
||||||
// No explicit attribute found. Try `__setattr__` as a fallback
|
// No explicit attribute found. Use `__setattr__` (already inferred
|
||||||
// for dynamic attribute assignment.
|
// above) as a fallback for dynamic attribute assignment.
|
||||||
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
|
|
||||||
db,
|
|
||||||
"__setattr__",
|
|
||||||
&mut CallArguments::positional([
|
|
||||||
Type::string_literal(db, attribute),
|
|
||||||
value_ty,
|
|
||||||
]),
|
|
||||||
TypeContext::default(),
|
|
||||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
|
||||||
);
|
|
||||||
|
|
||||||
let check_setattr_return_type = |result: &Bindings<'db>| -> bool {
|
|
||||||
match result.return_type(db) {
|
|
||||||
Type::Never => {
|
|
||||||
if emit_diagnostics {
|
|
||||||
if let Some(builder) = self
|
|
||||||
.context
|
|
||||||
.report_lint(&INVALID_ASSIGNMENT, target)
|
|
||||||
{
|
|
||||||
builder.into_diagnostic(format_args!(
|
|
||||||
"Cannot assign to unresolved attribute `{attribute}` on type `{}`",
|
|
||||||
object_ty.display(db)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match setattr_dunder_call_result {
|
match setattr_dunder_call_result {
|
||||||
Ok(result) => check_setattr_return_type(&result),
|
// If __setattr__ succeeded, allow the assignment.
|
||||||
Err(CallDunderError::PossiblyUnbound(result)) => {
|
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
|
||||||
check_setattr_return_type(&result)
|
|
||||||
}
|
|
||||||
Err(CallDunderError::CallError(..)) => {
|
Err(CallDunderError::CallError(..)) => {
|
||||||
if emit_diagnostics {
|
if emit_diagnostics {
|
||||||
if let Some(builder) =
|
if let Some(builder) =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue