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
|
||||
```
|
||||
|
||||
### `__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
|
||||
|
||||
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.
|
||||
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).
|
||||
// If so, block all attribute assignments regardless of explicit attributes.
|
||||
let setattr_returns_never = {
|
||||
let setattr_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,
|
||||
);
|
||||
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,
|
||||
}
|
||||
let setattr_returns_never = match &setattr_dunder_call_result {
|
||||
Ok(result) => result.return_type(db).is_never(),
|
||||
Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()),
|
||||
};
|
||||
|
||||
if setattr_returns_never {
|
||||
|
|
@ -4768,44 +4761,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
ensure_assignable_to(self, value_ty, instance_attr_ty)
|
||||
} else {
|
||||
// No explicit attribute found. Try `__setattr__` 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,
|
||||
}
|
||||
};
|
||||
|
||||
// No explicit attribute found. Use `__setattr__` (already inferred
|
||||
// above) as a fallback for dynamic attribute assignment.
|
||||
match setattr_dunder_call_result {
|
||||
Ok(result) => check_setattr_return_type(&result),
|
||||
Err(CallDunderError::PossiblyUnbound(result)) => {
|
||||
check_setattr_return_type(&result)
|
||||
}
|
||||
// If __setattr__ succeeded, allow the assignment.
|
||||
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
|
||||
Err(CallDunderError::CallError(..)) => {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
|
|
|
|||
Loading…
Reference in New Issue