mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Disallow assignment to Final
class attributes (#19457)
## Summary Emit errors for the following assignments: ```py class C: CLASS_LEVEL_CONSTANT: Final[int] = 1 C.CLASS_LEVEL_CONSTANT = 2 C().CLASS_LEVEL_CONSTANT = 2 ``` ## Test Plan Updated and new MD tests
This commit is contained in:
parent
f063c0e874
commit
b6579eaf04
3 changed files with 63 additions and 20 deletions
|
@ -498,7 +498,7 @@ class C:
|
||||||
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
|
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
|
||||||
|
|
||||||
c = C(1)
|
c = C(1)
|
||||||
# TODO: this should be an error
|
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
|
||||||
c.instance_variable = 2
|
c.instance_variable = 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -170,26 +170,37 @@ Assignments to attributes qualified with `Final` are also not allowed:
|
||||||
```py
|
```py
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
class C:
|
class Meta(type):
|
||||||
FINAL_A: Final[int] = 1
|
META_FINAL_A: Final[int] = 1
|
||||||
FINAL_B: Final = 1
|
META_FINAL_B: Final = 1
|
||||||
|
|
||||||
|
class C(metaclass=Meta):
|
||||||
|
CLASS_FINAL_A: Final[int] = 1
|
||||||
|
CLASS_FINAL_B: Final = 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.FINAL_C: Final[int] = 1
|
self.INSTANCE_FINAL_A: Final[int] = 1
|
||||||
self.FINAL_D: Final = 1
|
self.INSTANCE_FINAL_B: Final = 1
|
||||||
|
|
||||||
# TODO: these should be errors (that mention `Final`)
|
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
|
||||||
C.FINAL_A = 2
|
C.META_FINAL_A = 2
|
||||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type `<class 'C'>`"
|
||||||
C.FINAL_B = 2
|
C.META_FINAL_B = 2
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `<class 'C'>`"
|
||||||
|
C.CLASS_FINAL_A = 2
|
||||||
|
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `<class 'C'>`"
|
||||||
|
C.CLASS_FINAL_B = 2
|
||||||
|
|
||||||
# TODO: these should be errors (that mention `Final`)
|
|
||||||
c = C()
|
c = C()
|
||||||
c.FINAL_A = 2
|
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `C`"
|
||||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
c.CLASS_FINAL_A = 2
|
||||||
c.FINAL_B = 2
|
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`"
|
||||||
c.FINAL_C = 2
|
c.CLASS_FINAL_B = 2
|
||||||
c.FINAL_D = 2
|
# TODO: this should be an error
|
||||||
|
c.INSTANCE_FINAL_A = 2
|
||||||
|
# TODO: this should be an error
|
||||||
|
c.INSTANCE_FINAL_B = 2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mutability
|
## Mutability
|
||||||
|
|
|
@ -3363,6 +3363,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
assignable
|
assignable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute.
|
||||||
|
let invalid_assignment_to_final = |qualifiers: TypeQualifiers| -> bool {
|
||||||
|
if qualifiers.contains(TypeQualifiers::FINAL) {
|
||||||
|
if emit_diagnostics {
|
||||||
|
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
|
||||||
|
builder.into_diagnostic(format_args!(
|
||||||
|
"Cannot assign to final attribute `{attribute}` \
|
||||||
|
on type `{}`",
|
||||||
|
object_ty.display(db)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match object_ty {
|
match object_ty {
|
||||||
Type::Union(union) => {
|
Type::Union(union) => {
|
||||||
if union.elements(self.db()).iter().all(|elem| {
|
if union.elements(self.db()).iter().all(|elem| {
|
||||||
|
@ -3558,8 +3576,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
PlaceAndQualifiers {
|
PlaceAndQualifiers {
|
||||||
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
||||||
qualifiers: _,
|
qualifiers,
|
||||||
} => {
|
} => {
|
||||||
|
if invalid_assignment_to_final(qualifiers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let assignable_to_meta_attr =
|
let assignable_to_meta_attr =
|
||||||
if let Place::Type(meta_dunder_set, _) =
|
if let Place::Type(meta_dunder_set, _) =
|
||||||
meta_attr_ty.class_member(db, "__set__".into()).place
|
meta_attr_ty.class_member(db, "__set__".into()).place
|
||||||
|
@ -3669,8 +3691,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
match object_ty.class_member(db, attribute.into()) {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
PlaceAndQualifiers {
|
PlaceAndQualifiers {
|
||||||
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
||||||
qualifiers: _,
|
qualifiers,
|
||||||
} => {
|
} => {
|
||||||
|
if invalid_assignment_to_final(qualifiers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
|
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
|
||||||
meta_attr_ty.class_member(db, "__set__".into()).place
|
meta_attr_ty.class_member(db, "__set__".into()).place
|
||||||
{
|
{
|
||||||
|
@ -3733,11 +3759,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
place: Place::Unbound,
|
place: Place::Unbound,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty
|
if let PlaceAndQualifiers {
|
||||||
|
place: Place::Type(class_attr_ty, class_attr_boundness),
|
||||||
|
qualifiers,
|
||||||
|
} = object_ty
|
||||||
.find_name_in_mro(db, attribute)
|
.find_name_in_mro(db, attribute)
|
||||||
.expect("called on Type::ClassLiteral or Type::SubclassOf")
|
.expect("called on Type::ClassLiteral or Type::SubclassOf")
|
||||||
.place
|
|
||||||
{
|
{
|
||||||
|
if invalid_assignment_to_final(qualifiers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if class_attr_boundness == Boundness::PossiblyUnbound {
|
if class_attr_boundness == Boundness::PossiblyUnbound {
|
||||||
report_possibly_unbound_attribute(
|
report_possibly_unbound_attribute(
|
||||||
&self.context,
|
&self.context,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue