Skip to content

Commit b6579ea

Browse files
authored
[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
1 parent f063c0e commit b6579ea

File tree

3 files changed

+63
-20
lines changed

3 files changed

+63
-20
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ class C:
498498
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
499499

500500
c = C(1)
501-
# TODO: this should be an error
501+
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
502502
c.instance_variable = 2
503503
```
504504

crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,37 @@ Assignments to attributes qualified with `Final` are also not allowed:
170170
```py
171171
from typing import Final
172172

173-
class C:
174-
FINAL_A: Final[int] = 1
175-
FINAL_B: Final = 1
173+
class Meta(type):
174+
META_FINAL_A: Final[int] = 1
175+
META_FINAL_B: Final = 1
176+
177+
class C(metaclass=Meta):
178+
CLASS_FINAL_A: Final[int] = 1
179+
CLASS_FINAL_B: Final = 1
176180

177181
def __init__(self):
178-
self.FINAL_C: Final[int] = 1
179-
self.FINAL_D: Final = 1
182+
self.INSTANCE_FINAL_A: Final[int] = 1
183+
self.INSTANCE_FINAL_B: Final = 1
184+
185+
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
186+
C.META_FINAL_A = 2
187+
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type `<class 'C'>`"
188+
C.META_FINAL_B = 2
180189

181-
# TODO: these should be errors (that mention `Final`)
182-
C.FINAL_A = 2
183-
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
184-
C.FINAL_B = 2
190+
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `<class 'C'>`"
191+
C.CLASS_FINAL_A = 2
192+
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `<class 'C'>`"
193+
C.CLASS_FINAL_B = 2
185194

186-
# TODO: these should be errors (that mention `Final`)
187195
c = C()
188-
c.FINAL_A = 2
189-
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
190-
c.FINAL_B = 2
191-
c.FINAL_C = 2
192-
c.FINAL_D = 2
196+
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `C`"
197+
c.CLASS_FINAL_A = 2
198+
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`"
199+
c.CLASS_FINAL_B = 2
200+
# TODO: this should be an error
201+
c.INSTANCE_FINAL_A = 2
202+
# TODO: this should be an error
203+
c.INSTANCE_FINAL_B = 2
193204
```
194205

195206
## Mutability

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,6 +3363,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
33633363
assignable
33643364
};
33653365

3366+
// Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute.
3367+
let invalid_assignment_to_final = |qualifiers: TypeQualifiers| -> bool {
3368+
if qualifiers.contains(TypeQualifiers::FINAL) {
3369+
if emit_diagnostics {
3370+
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
3371+
builder.into_diagnostic(format_args!(
3372+
"Cannot assign to final attribute `{attribute}` \
3373+
on type `{}`",
3374+
object_ty.display(db)
3375+
));
3376+
}
3377+
}
3378+
true
3379+
} else {
3380+
false
3381+
}
3382+
};
3383+
33663384
match object_ty {
33673385
Type::Union(union) => {
33683386
if union.elements(self.db()).iter().all(|elem| {
@@ -3558,8 +3576,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
35583576
}
35593577
PlaceAndQualifiers {
35603578
place: Place::Type(meta_attr_ty, meta_attr_boundness),
3561-
qualifiers: _,
3579+
qualifiers,
35623580
} => {
3581+
if invalid_assignment_to_final(qualifiers) {
3582+
return false;
3583+
}
3584+
35633585
let assignable_to_meta_attr =
35643586
if let Place::Type(meta_dunder_set, _) =
35653587
meta_attr_ty.class_member(db, "__set__".into()).place
@@ -3669,8 +3691,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
36693691
match object_ty.class_member(db, attribute.into()) {
36703692
PlaceAndQualifiers {
36713693
place: Place::Type(meta_attr_ty, meta_attr_boundness),
3672-
qualifiers: _,
3694+
qualifiers,
36733695
} => {
3696+
if invalid_assignment_to_final(qualifiers) {
3697+
return false;
3698+
}
3699+
36743700
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
36753701
meta_attr_ty.class_member(db, "__set__".into()).place
36763702
{
@@ -3733,11 +3759,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
37333759
place: Place::Unbound,
37343760
..
37353761
} => {
3736-
if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty
3762+
if let PlaceAndQualifiers {
3763+
place: Place::Type(class_attr_ty, class_attr_boundness),
3764+
qualifiers,
3765+
} = object_ty
37373766
.find_name_in_mro(db, attribute)
37383767
.expect("called on Type::ClassLiteral or Type::SubclassOf")
3739-
.place
37403768
{
3769+
if invalid_assignment_to_final(qualifiers) {
3770+
return false;
3771+
}
3772+
37413773
if class_attr_boundness == Boundness::PossiblyUnbound {
37423774
report_possibly_unbound_attribute(
37433775
&self.context,

0 commit comments

Comments
 (0)