mirror of https://github.com/astral-sh/ruff
Raise diagnostic when frozen dataclass inherits a non-frozen dataclass and the other way around
This commit is contained in:
parent
82a7598aa8
commit
6368fd0ea5
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue