From 6368fd0ea54d8b2ad863814dfb85ba0afafb2823 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:49:10 +0100 Subject: [PATCH 1/5] Raise diagnostic when frozen dataclass inherits a non-frozen dataclass and the other way around --- .../src/types/diagnostic.rs | 60 ++++++++++++++++ .../src/types/infer/builder.rs | 72 +++++++++++-------- 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index dcc2f6b3c55e0..47767156a491d 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 1b78a02c32f6e..340728572ed3a 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 From d35e7f313db52d15c6eb7f4bbc7b58e9380e5e7d Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:56:26 +0100 Subject: [PATCH 2/5] Add tests, address review --- .../mdtest/dataclasses/dataclasses.md | 30 +++++++++++ .../src/types/diagnostic.rs | 10 ++-- .../src/types/infer/builder.rs | 53 +++++++++++++++---- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 7c6417565814d..4c2cd6ed90f9f 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 47767156a491d..6b2dbbfedbdc3 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 340728572ed3a..a4b97b729db4a 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) { From 19abe7fc0d8e76203fc3e805788cd5fda426baf0 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:47:06 +0100 Subject: [PATCH 3/5] Combine diagnostics, fix docs --- crates/ty/docs/rules.md | 192 +++++++++++------- .../mdtest/dataclasses/dataclasses.md | 4 +- .../src/types/diagnostic.rs | 39 +--- .../src/types/infer/builder.rs | 34 +--- .../e2e__commands__debug_command.snap | 1 + ty.schema.json | 10 + 6 files changed, 144 insertions(+), 136 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index e32e8d3e53fc5..c2b450ae10bd3 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -787,13 +787,57 @@ class D(A): def foo(self): ... # fine: overrides `A.foo` ``` +## `invalid-frozen-dataclass-subclass` + + +Default level: error · +Added in 0.0.1-alpha.35 · +Related issues · +View source + + + +**What it does** + +Checks for dataclasses with invalid frozen inheritance: +- A frozen dataclass cannot inherit from a non-frozen dataclass. +- A non-frozen dataclass cannot inherit from a frozen dataclass. + +**Why is this bad?** + +Python raises a `TypeError` at runtime when either of these inheritance +patterns occurs. + +**Example** + + +```python +from dataclasses import dataclass + +@dataclass +class Base: + x: int + +@dataclass(frozen=True) +class Child(Base): # Error raised here + y: int + +@dataclass(frozen=True) +class FrozenBase: + x: int + +@dataclass +class NonFrozenChild(FrozenBase): # Error raised here + y: int +``` + ## `invalid-generic-class` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +870,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +909,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +944,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +978,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1085,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1095,7 +1139,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1125,7 +1169,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1175,7 +1219,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1201,7 +1245,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1232,7 +1276,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1266,7 +1310,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1315,7 +1359,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1340,7 +1384,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1398,7 +1442,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1425,7 +1469,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1472,7 +1516,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1502,7 +1546,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1532,7 +1576,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1566,7 +1610,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1600,7 +1644,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1635,7 +1679,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1660,7 +1704,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1693,7 +1737,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1722,7 +1766,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1746,7 +1790,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1772,7 +1816,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1805,7 +1849,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1832,7 +1876,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1890,7 +1934,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1920,7 +1964,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1993,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -1983,7 +2027,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2010,7 +2054,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2038,7 +2082,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2084,7 +2128,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2111,7 +2155,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2139,7 +2183,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2164,7 +2208,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2189,7 +2233,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2226,7 +2270,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2254,7 +2298,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2279,7 +2323,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2320,7 +2364,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2408,7 +2452,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2436,7 +2480,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2468,7 +2512,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2500,7 +2544,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2571,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2551,7 +2595,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2609,7 +2653,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2648,7 +2692,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2711,7 +2755,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2735,7 +2779,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 4c2cd6ed90f9f..54942f4a2e21b 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -531,7 +531,7 @@ 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" +class Child(FrozenBase): # error: [invalid-frozen-dataclass-subclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen" y: int ``` @@ -546,7 +546,7 @@ 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" +class FrozenChild(Base): # error: [invalid-frozen-dataclass-subclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen" y: int ``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6b2dbbfedbdc3..abd39ee2b41aa 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -121,8 +121,7 @@ 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); + registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS); // String annotations registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); @@ -2224,11 +2223,13 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for frozen dataclasses that inherit from non-frozen dataclasses. + /// Checks for dataclasses with invalid frozen inheritance: + /// - A frozen dataclass cannot inherit from a non-frozen dataclass. + /// - A non-frozen dataclass cannot inherit from a frozen dataclass. /// /// ## Why is this bad? - /// Python raises a `TypeError` at runtime when a frozen dataclass - /// inherits from a non-frozen dataclass. + /// Python raises a `TypeError` at runtime when either of these inheritance + /// patterns occurs. /// /// ## Example /// @@ -2242,37 +2243,17 @@ declare_lint! { /// @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? - /// Python raises a `TypeError` at runtime when a non-frozen dataclass - /// inherits from a frozen dataclass. - /// - /// ## Example - /// - /// ```python - /// from dataclasses import dataclass /// /// @dataclass(frozen=True) - /// class Base: + /// class FrozenBase: /// x: int /// /// @dataclass - /// class Child(Base): # Error raised here + /// class NonFrozenChild(FrozenBase): # Error raised here /// y: int /// ``` - pub(crate) static NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS = { - summary: "detects non-frozen dataclasses inheriting from frozen dataclasses", + pub(crate) static INVALID_FROZEN_DATACLASS_SUBCLASS = { + summary: "detects dataclasses with invalid frozen/non-frozen subclassing", status: LintStatus::stable("0.0.1-alpha.35"), default_level: Level::Error, } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a4b97b729db4a..a395949ba6686 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,35 +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, - 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::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INVALID_FROZEN_DATACLASS_SUBCLASS, 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::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, is_implicit_classmethod, is_implicit_staticmethod, @@ -775,7 +747,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match (base_is_frozen, class_is_frozen) { (true, false) => { if let Some(builder) = self.context.report_lint( - &NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS, + &INVALID_FROZEN_DATACLASS_SUBCLASS, &class_node.bases()[i], ) { builder.into_diagnostic(format_args!( @@ -787,7 +759,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } (false, true) => { if let Some(builder) = self.context.report_lint( - &FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, + &INVALID_FROZEN_DATACLASS_SUBCLASS, &class_node.bases()[i], ) { builder.into_diagnostic(format_args!( diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 21c4b0501433a..cf1b0afc645a6 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -55,6 +55,7 @@ Settings: Settings { "invalid-declaration": Error (Default), "invalid-exception-caught": Error (Default), "invalid-explicit-override": Error (Default), + "invalid-frozen-dataclass-subclass": Error (Default), "invalid-generic-class": Error (Default), "invalid-ignore-comment": Warning (Default), "invalid-key": Error (Default), diff --git a/ty.schema.json b/ty.schema.json index 13d1ac9c478fc..87feeb250775c 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -583,6 +583,16 @@ } ] }, + "invalid-frozen-dataclass-subclass": { + "title": "detects dataclasses with invalid frozen/non-frozen subclassing", + "description": "## What it does\nChecks for dataclasses with invalid frozen inheritance:\n- A frozen dataclass cannot inherit from a non-frozen dataclass.\n- A non-frozen dataclass cannot inherit from a frozen dataclass.\n\n## Why is this bad?\nPython raises a `TypeError` at runtime when either of these inheritance\npatterns occurs.\n\n## Example\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass Base:\n x: int\n\n@dataclass(frozen=True)\nclass Child(Base): # Error raised here\n y: int\n\n@dataclass(frozen=True)\nclass FrozenBase:\n x: int\n\n@dataclass\nclass NonFrozenChild(FrozenBase): # Error raised here\n y: int\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-generic-class": { "title": "detects invalid generic classes", "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", From 286649be5bec15cf2a5e3d61d20d386afd610904 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:50:22 +0100 Subject: [PATCH 4/5] Formatting --- .../src/types/infer/builder.rs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a395949ba6686..b6ed9aa2c6f00 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,7 +55,34 @@ 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, INVALID_FROZEN_DATACLASS_SUBCLASS, 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_FROZEN_DATACLASS_SUBCLASS, 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::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, is_implicit_classmethod, is_implicit_staticmethod, From 4dfad38c753e28ff1fca4c961097abcb9f19d4e1 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 13 Dec 2025 20:49:40 +0000 Subject: [PATCH 5/5] fancier diagnostics --- .../mdtest/dataclasses/dataclasses.md | 51 +++++- ...zen_in\342\200\246_(9af2ab07b8e829e).snap" | 154 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 6 + crates/ty_python_semantic/src/types/class.rs | 22 +++ .../src/types/diagnostic.rs | 90 +++++++++- .../src/types/infer/builder.rs | 90 ++++------ 6 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 54942f4a2e21b..60bfd36176948 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -521,7 +521,14 @@ 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: +### frozen/non-frozen inheritance + +If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We +catch this error: + + + +`a.py`: ```py from dataclasses import dataclass @@ -530,13 +537,15 @@ from dataclasses import dataclass class FrozenBase: x: int -@dataclass -class Child(FrozenBase): # error: [invalid-frozen-dataclass-subclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen" - +@dataclass +# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`" +class Child(FrozenBase): y: int ``` -A diagnostic is emitted if a frozen dataclass inherits from a non-frozen dataclass: +Frozen dataclasses inheriting from non-frozen dataclasses are also illegal: + +`b.py`: ```py from dataclasses import dataclass @@ -546,11 +555,39 @@ class Base: x: int @dataclass(frozen=True) -class FrozenChild(Base): # error: [invalid-frozen-dataclass-subclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen" - +# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`" +class FrozenChild(Base): y: int ``` +Example of diagnostics when there are multiple files involved: + +`module.py`: + +```py +import dataclasses + +@dataclasses.dataclass(frozen=False) +class NotFrozenBase: + x: int +``` + +`main.py`: + +```py +from functools import total_ordering +from typing import final +from dataclasses import dataclass + +from module import NotFrozenBase + +@final +@dataclass(frozen=True) +@total_ordering +class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass] + y: str +``` + ### `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/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap" new file mode 100644 index 0000000000000..3b3a006b6e19a --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap" @@ -0,0 +1,154 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: dataclasses.md - Dataclasses - Other dataclass parameters - frozen/non-frozen inheritance +mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +--- + +# Python source files + +## a.py + +``` + 1 | from dataclasses import dataclass + 2 | + 3 | @dataclass(frozen=True) + 4 | class FrozenBase: + 5 | x: int + 6 | + 7 | @dataclass + 8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`" + 9 | class Child(FrozenBase): +10 | y: int +``` + +## b.py + +``` + 1 | from dataclasses import dataclass + 2 | + 3 | @dataclass + 4 | class Base: + 5 | x: int + 6 | + 7 | @dataclass(frozen=True) + 8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`" + 9 | class FrozenChild(Base): +10 | y: int +``` + +## module.py + +``` +1 | import dataclasses +2 | +3 | @dataclasses.dataclass(frozen=False) +4 | class NotFrozenBase: +5 | x: int +``` + +## main.py + +``` + 1 | from functools import total_ordering + 2 | from typing import final + 3 | from dataclasses import dataclass + 4 | + 5 | from module import NotFrozenBase + 6 | + 7 | @final + 8 | @dataclass(frozen=True) + 9 | @total_ordering +10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass] +11 | y: str +``` + +# Diagnostics + +``` +error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass + --> src/a.py:7:1 + | + 5 | x: int + 6 | + 7 | @dataclass + | ---------- `Child` dataclass parameters + 8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`" + 9 | class Child(FrozenBase): + | ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is +10 | y: int + | +info: This causes the class creation to fail +info: Base class definition + --> src/a.py:3:1 + | +1 | from dataclasses import dataclass +2 | +3 | @dataclass(frozen=True) + | ----------------------- `FrozenBase` dataclass parameters +4 | class FrozenBase: + | ^^^^^^^^^^ `FrozenBase` definition +5 | x: int + | +info: rule `invalid-frozen-dataclass-subclass` is enabled by default + +``` + +``` +error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass + --> src/b.py:7:1 + | + 5 | x: int + 6 | + 7 | @dataclass(frozen=True) + | ----------------------- `FrozenChild` dataclass parameters + 8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`" + 9 | class FrozenChild(Base): + | ^^^^^^^^^^^^----^ Subclass `FrozenChild` is frozen but base class `Base` is not +10 | y: int + | +info: This causes the class creation to fail +info: Base class definition + --> src/b.py:3:1 + | +1 | from dataclasses import dataclass +2 | +3 | @dataclass + | ---------- `Base` dataclass parameters +4 | class Base: + | ^^^^ `Base` definition +5 | x: int + | +info: rule `invalid-frozen-dataclass-subclass` is enabled by default + +``` + +``` +error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass + --> src/main.py:8:1 + | + 7 | @final + 8 | @dataclass(frozen=True) + | ----------------------- `FrozenChild` dataclass parameters + 9 | @total_ordering +10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass] + | ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not +11 | y: str + | +info: This causes the class creation to fail +info: Base class definition + --> src/module.py:3:1 + | +1 | import dataclasses +2 | +3 | @dataclasses.dataclass(frozen=False) + | ------------------------------------ `NotFrozenBase` dataclass parameters +4 | class NotFrozenBase: + | ^^^^^^^^^^^^^ `NotFrozenBase` definition +5 | x: int + | +info: rule `invalid-frozen-dataclass-subclass` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e9311547d7d2b..726decfc07fd9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -677,6 +677,12 @@ bitflags! { } } +impl DataclassFlags { + pub(crate) const fn is_frozen(self) -> bool { + self.contains(Self::FROZEN) + } +} + pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[ ("init", DataclassFlags::INIT), ("repr", DataclassFlags::REPR), diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 32962ea128090..022e63fe33431 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1856,6 +1856,28 @@ impl<'db> ClassLiteral<'db> { .filter_map(|decorator| decorator.known(db)) } + /// Iterate through the decorators on this class, returning the position of the first one + /// that matches the given predicate. + pub(super) fn find_decorator_position( + self, + db: &'db dyn Db, + predicate: impl Fn(Type<'db>) -> bool, + ) -> Option { + self.decorators(db) + .iter() + .position(|decorator| predicate(*decorator)) + } + + /// Iterate through the decorators on this class, returning the index of the first one + /// that is either `@dataclass` or `@dataclass(...)`. + pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { + self.find_decorator_position(db, |ty| match ty { + Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), + Type::DataclassDecorator(_) => true, + _ => false, + }) + } + /// Is this class final? pub(super) fn is_final(self, db: &'db dyn Db) -> bool { self.known_function_decorators(db) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index abd39ee2b41aa..3acd7b0a64191 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -30,7 +30,7 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, protocol_class::ProtocolClass, }; -use crate::types::{KnownInstanceType, MemberLookupPolicy}; +use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy}; use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ @@ -4308,6 +4308,94 @@ fn report_unsupported_binary_operation_impl<'a>( Some(diagnostic) } +pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( + context: &InferContext<'db, '_>, + class: ClassLiteral<'db>, + class_node: &ast::StmtClassDef, + base_class: ClassLiteral<'db>, + base_class_node: &ast::Expr, + base_class_params: DataclassFlags, +) { + let db = context.db(); + + let Some(builder) = + context.report_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS, class.header_range(db)) + else { + return; + }; + + let mut diagnostic = if base_class_params.is_frozen() { + let mut diagnostic = + builder.into_diagnostic("Non-frozen dataclass cannot inherit from frozen dataclass"); + diagnostic.set_concise_message(format_args!( + "Non-frozen dataclass `{}` cannot inherit from frozen dataclass `{}`", + class.name(db), + base_class.name(db) + )); + diagnostic.set_primary_message(format_args!( + "Subclass `{}` is not frozen but base class `{}` is", + class.name(db), + base_class.name(db) + )); + diagnostic + } else { + let mut diagnostic = + builder.into_diagnostic("Frozen dataclass cannot inherit from non-frozen dataclass"); + diagnostic.set_concise_message(format_args!( + "Frozen dataclass `{}` cannot inherit from non-frozen dataclass `{}`", + class.name(db), + base_class.name(db) + )); + diagnostic.set_primary_message(format_args!( + "Subclass `{}` is frozen but base class `{}` is not", + class.name(db), + base_class.name(db) + )); + diagnostic + }; + + diagnostic.annotate(context.secondary(base_class_node)); + + if let Some(position) = class.find_dataclass_decorator_position(db) { + diagnostic.annotate( + context + .secondary(&class_node.decorator_list[position]) + .message(format_args!("`{}` dataclass parameters", class.name(db))), + ); + } + diagnostic.info("This causes the class creation to fail"); + + if let Some(decorator_position) = base_class.find_dataclass_decorator_position(db) { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("Base class definition"), + ); + sub.annotate( + Annotation::primary(base_class.header_span(db)) + .message(format_args!("`{}` definition", base_class.name(db))), + ); + + let base_class_file = base_class.file(db); + let module = parsed_module(db, base_class_file).load(db); + + let decorator_range = base_class + .body_scope(db) + .node(db) + .expect_class() + .node(&module) + .decorator_list[decorator_position] + .range(); + + sub.annotate( + Annotation::secondary(Span::from(base_class_file).with_range(decorator_range)).message( + format_args!("`{}` dataclass parameters", base_class.name(db)), + ), + ); + + diagnostic.sub(sub); + } +} + /// This function receives an unresolved `from foo import bar` import, /// where `foo` can be resolved to a module but that module does not /// have a `bar` member or submodule. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b6ed9aa2c6f00..43940a4d889d4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -59,25 +59,26 @@ 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_FROZEN_DATACLASS_SUBCLASS, 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_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_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, + 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, @@ -104,15 +105,14 @@ 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, 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}; @@ -763,40 +763,18 @@ 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()) - .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( - &INVALID_FROZEN_DATACLASS_SUBCLASS, - &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( - &INVALID_FROZEN_DATACLASS_SUBCLASS, - &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()), - )); - } - } - _ => {} + let base_params = base_params.flags(self.db()); + let class_is_frozen = class_params.flags(self.db()).is_frozen(); + + if base_params.is_frozen() != class_is_frozen { + report_bad_frozen_dataclass_inheritance( + &self.context, + class, + class_node, + base_class_literal, + &class_node.bases()[i], + base_params, + ); } } }