mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[ty] Detect illegal multiple inheritance with NamedTuple
(#19943)
This commit is contained in:
parent
5e4fa9e442
commit
fbf24be8ae
7 changed files with 250 additions and 76 deletions
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue