diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index af54a7cd33..e3fd368b1e 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -498,7 +498,7 @@ class C: reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None 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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index 13c5272928..ed797f5e0b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -170,26 +170,37 @@ Assignments to attributes qualified with `Final` are also not allowed: ```py from typing import Final -class C: - FINAL_A: Final[int] = 1 - FINAL_B: Final = 1 +class Meta(type): + META_FINAL_A: Final[int] = 1 + META_FINAL_B: Final = 1 + +class C(metaclass=Meta): + CLASS_FINAL_A: Final[int] = 1 + CLASS_FINAL_B: Final = 1 def __init__(self): - self.FINAL_C: Final[int] = 1 - self.FINAL_D: Final = 1 + self.INSTANCE_FINAL_A: Final[int] = 1 + self.INSTANCE_FINAL_B: Final = 1 -# TODO: these should be errors (that mention `Final`) -C.FINAL_A = 2 -# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`" -C.FINAL_B = 2 +# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type ``" +C.META_FINAL_A = 2 +# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type ``" +C.META_FINAL_B = 2 + +# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type ``" +C.CLASS_FINAL_A = 2 +# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type ``" +C.CLASS_FINAL_B = 2 -# TODO: these should be errors (that mention `Final`) c = C() -c.FINAL_A = 2 -# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`" -c.FINAL_B = 2 -c.FINAL_C = 2 -c.FINAL_D = 2 +# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_A` on type `C`" +c.CLASS_FINAL_A = 2 +# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`" +c.CLASS_FINAL_B = 2 +# TODO: this should be an error +c.INSTANCE_FINAL_A = 2 +# TODO: this should be an error +c.INSTANCE_FINAL_B = 2 ``` ## Mutability diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ba9df15c14..86e3012435 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -3363,6 +3363,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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 { Type::Union(union) => { if union.elements(self.db()).iter().all(|elem| { @@ -3558,8 +3576,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } PlaceAndQualifiers { 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, _) = 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()) { PlaceAndQualifiers { 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, _) = meta_attr_ty.class_member(db, "__set__".into()).place { @@ -3733,11 +3759,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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) .expect("called on Type::ClassLiteral or Type::SubclassOf") - .place { + if invalid_assignment_to_final(qualifiers) { + return false; + } + if class_attr_boundness == Boundness::PossiblyUnbound { report_possibly_unbound_attribute( &self.context,