diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 7c64175658..4c2cd6ed90 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -521,6 +521,36 @@ frozen = MyFrozenChildClass() del frozen.x # TODO this should emit an [invalid-assignment] ``` +A diagnostic is emitted if a non-frozen dataclass inherits from a frozen dataclass: + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class FrozenBase: + x: int + +@dataclass +class Child(FrozenBase): # error: [non-frozen-subclass-of-frozen-dataclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen" + + y: int +``` + +A diagnostic is emitted if a frozen dataclass inherits from a non-frozen dataclass: + +```py +from dataclasses import dataclass + +@dataclass +class Base: + x: int + +@dataclass(frozen=True) +class FrozenChild(Base): # error: [frozen-subclass-of-non-frozen-dataclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen" + + y: int +``` + ### `match_args` If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 47767156a4..6b2dbbfedb 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -2227,8 +2227,8 @@ declare_lint! { /// Checks for frozen dataclasses that inherit from non-frozen dataclasses. /// /// ## Why is this bad? - /// A frozen dataclass promises immutability. Inheriting from a non-frozen - /// dataclass breaks that guarantee because the base class allows mutation. + /// Python raises a `TypeError` at runtime when a frozen dataclass + /// inherits from a non-frozen dataclass. /// /// ## Example /// @@ -2255,8 +2255,8 @@ declare_lint! { /// Checks for non-frozen dataclasses that inherit from frozen dataclasses. /// /// ## Why is this bad? - /// A frozen dataclass enforces immutability. Allowing a non-frozen subclass - /// would reintroduce mutability and violate the base class contract. + /// Python raises a `TypeError` at runtime when a non-frozen dataclass + /// inherits from a frozen dataclass. /// /// ## Example /// @@ -2278,8 +2278,6 @@ declare_lint! { } } - - /// A collection of type check diagnostics. #[derive(Default, Eq, PartialEq, get_size2::GetSize)] pub struct TypeCheckDiagnostics { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 340728572e..a4b97b729d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,7 +55,35 @@ use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorK use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator}; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; -use crate::types::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison, FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS}; +use crate::types::diagnostic::{ + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, + CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, + FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, + INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, + INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, + INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, + INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, + NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, + UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + hint_if_stdlib_attribute_exists_on_other_versions, + hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, + report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, + report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, + report_instance_layout_conflict, report_invalid_arguments_to_annotated, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_exception_caught, report_invalid_exception_cause, + report_invalid_exception_raised, report_invalid_exception_tuple_caught, + report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, + report_invalid_or_unsupported_base, report_invalid_return_type, + report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, + report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, + report_possibly_missing_attribute, report_possibly_unresolved_reference, + report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, + report_unsupported_binary_operation, report_unsupported_comparison, +}; use crate::types::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, is_implicit_classmethod, is_implicit_staticmethod, @@ -77,14 +105,15 @@ use crate::types::typed_dict::{ use crate::types::visitor::any_over_type; use crate::types::{ BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind, - ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, - MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, - ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, TrackedConstraintSet, - Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, - TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, - TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, UnionTypeInstance, binding_type, infer_scope_types, todo_type, + ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, + ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, + SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, + TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, + TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, + TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types, + todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -735,10 +764,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { base_class_literal.dataclass_params(self.db()), class.dataclass_params(self.db()), ) { - let base_is_frozen = base_params.flags(self.db()) + let base_is_frozen = base_params + .flags(self.db()) .contains(DataclassFlags::FROZEN); - let class_is_frozen = class_params.flags(self.db()) + let class_is_frozen = class_params + .flags(self.db()) .contains(DataclassFlags::FROZEN); match (base_is_frozen, class_is_frozen) {