[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

@ -115,15 +115,28 @@ class Location(NamedTuple):
### Multiple Inheritance
Multiple inheritance is not supported for `NamedTuple` classes:
<!-- snapshot-diagnostics -->
Multiple inheritance is not supported for `NamedTuple` classes except with `Generic`:
```py
from typing import NamedTuple
from typing import NamedTuple, Protocol
# This should ideally emit a diagnostic
# error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
class C(NamedTuple, object):
id: int
name: str
# fmt: off
class D(
int, # error: [invalid-named-tuple]
NamedTuple
): ...
# fmt: on
# error: [invalid-named-tuple]
class E(NamedTuple, Protocol): ...
```
### Inheriting from a `NamedTuple`

View file

@ -0,0 +1,73 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: named_tuple.md - `NamedTuple` - `typing.NamedTuple` - Multiple Inheritance
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import NamedTuple, Protocol
2 |
3 | # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
4 | class C(NamedTuple, object):
5 | id: int
6 |
7 | # fmt: off
8 |
9 | class D(
10 | int, # error: [invalid-named-tuple]
11 | NamedTuple
12 | ): ...
13 |
14 | # fmt: on
15 |
16 | # error: [invalid-named-tuple]
17 | class E(NamedTuple, Protocol): ...
```
# Diagnostics
```
error[invalid-named-tuple]: NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`
--> src/mdtest_snippet.py:4:21
|
3 | # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
4 | class C(NamedTuple, object):
| ^^^^^^
5 | id: int
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple class `D` cannot use multiple inheritance except with `Generic[]`
--> src/mdtest_snippet.py:10:5
|
9 | class D(
10 | int, # error: [invalid-named-tuple]
| ^^^
11 | NamedTuple
12 | ): ...
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple class `E` cannot use multiple inheritance except with `Generic[]`
--> src/mdtest_snippet.py:17:21
|
16 | # error: [invalid-named-tuple]
17 | class E(NamedTuple, Protocol): ...
| ^^^^^^^^
|
info: rule `invalid-named-tuple` is enabled by default
```

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