[ty] Detect illegal multiple inheritance with NamedTuple (#19943)

This commit is contained in:
Alex Waygood 2025-08-18 13:03:01 +01:00 committed by GitHub
parent 5e4fa9e442
commit fbf24be8ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 250 additions and 76 deletions

View file

@ -227,7 +227,7 @@ impl CodeGeneratorKind {
code_generator_of_class(db, class)
}
fn matches(self, db: &dyn Db, class: ClassLiteral<'_>) -> bool {
pub(super) fn matches(self, db: &dyn Db, class: ClassLiteral<'_>) -> bool {
CodeGeneratorKind::from_class(db, class) == Some(self)
}
}

View file

@ -57,6 +57,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_OVERLOAD);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_PROTOCOL);
registry.register_lint(&INVALID_NAMED_TUPLE);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_SUPER_ARGUMENT);
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
@ -448,6 +449,32 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for invalidly defined `NamedTuple` classes.
///
/// ## Why is this bad?
/// An invalidly defined `NamedTuple` class may lead to the type checker
/// drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
///
/// ## Examples
/// A class definition cannot combine `NamedTuple` with other base classes
/// in multiple inheritance; doing so raises a `TypeError` at runtime. The sole
/// exception to this rule is `Generic[]`, which can be used alongside `NamedTuple`
/// in a class's bases list.
///
/// ```pycon
/// >>> from typing import NamedTuple
/// >>> class Foo(NamedTuple, object): ...
/// TypeError: can only inherit from a NamedTuple type and Generic
/// ```
pub(crate) static INVALID_NAMED_TUPLE = {
summary: "detects invalid `NamedTuple` class definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for classes with an inconsistent [method resolution order] (MRO).

View file

@ -95,16 +95,17 @@ use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_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_PARAMETER_DEFAULT,
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
report_invalid_return_type, report_possibly_unbound_attribute,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_key_on_typed_dict, report_invalid_return_type,
report_possibly_unbound_attribute,
};
use crate::types::enums::is_enum_class;
use crate::types::function::{
@ -1110,13 +1111,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
let is_protocol = class.is_protocol(self.db());
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class);
let mut solid_bases = IncompatibleBases::default();
// (2) Iterate through the class's explicit bases to check for various possible errors:
// - Check for inheritance from plain `Generic`,
// - Check for inheritance from a `@final` classes
// - If the class is a protocol class: check for inheritance from a non-protocol class
// - If the class is a NamedTuple class: check for multiple inheritance that isn't `Generic[]`
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
if is_named_tuple
&& !matches!(
base_class,
Type::SpecialForm(SpecialFormType::NamedTuple)
| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_))
)
{
if let Some(builder) = self
.context
.report_lint(&INVALID_NAMED_TUPLE, &class_node.bases()[i])
{
builder.into_diagnostic(format_args!(
"NamedTuple class `{}` cannot use multiple inheritance except with `Generic[]`",
class.name(self.db()),
));
}
}
let base_class = match base_class {
Type::SpecialForm(SpecialFormType::Generic) => {
if let Some(builder) = self