diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index dcc2f6b3c5..47767156a4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -121,6 +121,8 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_METHOD_OVERRIDE); registry.register_lint(&INVALID_EXPLICIT_OVERRIDE); registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD); + registry.register_lint(&FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS); + registry.register_lint(&NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS); // String annotations registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); @@ -2220,6 +2222,64 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// 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. + /// + /// ## Example + /// + /// ```python + /// from dataclasses import dataclass + /// + /// @dataclass + /// class Base: + /// x: int + /// + /// @dataclass(frozen=True) + /// class Child(Base): # Error raised here + /// y: int + /// ``` + pub(crate) static FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS = { + summary: "detects frozen dataclasses inheriting from non-frozen dataclasses", + status: LintStatus::stable("0.0.1-alpha.35"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// 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. + /// + /// ## Example + /// + /// ```python + /// from dataclasses import dataclass + /// + /// @dataclass(frozen=True) + /// class Base: + /// x: int + /// + /// @dataclass + /// class Child(Base): # Error raised here + /// y: int + /// ``` + pub(crate) static NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS = { + summary: "detects non-frozen dataclasses inheriting from frozen dataclasses", + status: LintStatus::stable("0.0.1-alpha.35"), + default_level: Level::Error, + } +} + + + /// 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 1b78a02c32..340728572e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,34 +55,7 @@ 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, -}; +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::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, is_implicit_classmethod, is_implicit_staticmethod, @@ -104,7 +77,7 @@ use crate::types::typed_dict::{ use crate::types::visitor::any_over_type; use crate::types::{ BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind, - ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder, + ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, TrackedConstraintSet, @@ -755,6 +728,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } } + + let (base_class_literal, _) = base_class.class_literal(self.db()); + + if let (Some(base_params), Some(class_params)) = ( + base_class_literal.dataclass_params(self.db()), + class.dataclass_params(self.db()), + ) { + let base_is_frozen = base_params.flags(self.db()) + .contains(DataclassFlags::FROZEN); + + let class_is_frozen = class_params.flags(self.db()) + .contains(DataclassFlags::FROZEN); + + match (base_is_frozen, class_is_frozen) { + (true, false) => { + if let Some(builder) = self.context.report_lint( + &NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS, + &class_node.bases()[i], + ) { + builder.into_diagnostic(format_args!( + "A non-frozen class `{}` cannot inherit from a class `{}` that is frozen", + class.name(self.db()), + base_class.name(self.db()), + )); + } + } + (false, true) => { + if let Some(builder) = self.context.report_lint( + &FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, + &class_node.bases()[i], + ) { + builder.into_diagnostic(format_args!( + "A frozen class `{}` cannot inherit from a class `{}` that is not frozen", + class.name(self.db()), + base_class.name(self.db()), + )); + } + } + _ => {} + } + } } // (4) Check that the class's MRO is resolvable