Skip to content

Commit 81bccb3

Browse files
committed
[ty] Detect illegal multiple inheritance with NamedTuple
1 parent 9ac39ce commit 81bccb3

File tree

7 files changed

+264
-76
lines changed

7 files changed

+264
-76
lines changed

crates/ty/docs/rules.md

Lines changed: 98 additions & 61 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,28 @@ class Location(NamedTuple):
115115

116116
### Multiple Inheritance
117117

118-
Multiple inheritance is not supported for `NamedTuple` classes:
118+
<!-- snapshot-diagnostics -->
119+
120+
Multiple inheritance is not supported for `NamedTuple` classes except with `Generic`:
119121

120122
```py
121-
from typing import NamedTuple
123+
from typing import NamedTuple, Protocol
122124

123-
# This should ideally emit a diagnostic
125+
# error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
124126
class C(NamedTuple, object):
125127
id: int
126-
name: str
128+
129+
# fmt: off
130+
131+
class D(
132+
int, # error: [invalid-named-tuple]
133+
NamedTuple
134+
): ...
135+
136+
# fmt: on
137+
138+
# error: [invalid-named-tuple]
139+
class E(NamedTuple, Protocol): ...
127140
```
128141

129142
### Inheriting from a `NamedTuple`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: named_tuple.md - `NamedTuple` - `typing.NamedTuple` - Multiple Inheritance
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing import NamedTuple, Protocol
16+
2 |
17+
3 | # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
18+
4 | class C(NamedTuple, object):
19+
5 | id: int
20+
6 |
21+
7 | # fmt: off
22+
8 |
23+
9 | class D(
24+
10 | int, # error: [invalid-named-tuple]
25+
11 | NamedTuple
26+
12 | ): ...
27+
13 |
28+
14 | # fmt: on
29+
15 |
30+
16 | # error: [invalid-named-tuple]
31+
17 | class E(NamedTuple, Protocol): ...
32+
```
33+
34+
# Diagnostics
35+
36+
```
37+
error[invalid-named-tuple]: NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`
38+
--> src/mdtest_snippet.py:4:21
39+
|
40+
3 | # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
41+
4 | class C(NamedTuple, object):
42+
| ^^^^^^
43+
5 | id: int
44+
|
45+
info: rule `invalid-named-tuple` is enabled by default
46+
47+
```
48+
49+
```
50+
error[invalid-named-tuple]: NamedTuple class `D` cannot use multiple inheritance except with `Generic[]`
51+
--> src/mdtest_snippet.py:10:5
52+
|
53+
9 | class D(
54+
10 | int, # error: [invalid-named-tuple]
55+
| ^^^
56+
11 | NamedTuple
57+
12 | ): ...
58+
|
59+
info: rule `invalid-named-tuple` is enabled by default
60+
61+
```
62+
63+
```
64+
error[invalid-named-tuple]: NamedTuple class `E` cannot use multiple inheritance except with `Generic[]`
65+
--> src/mdtest_snippet.py:17:21
66+
|
67+
16 | # error: [invalid-named-tuple]
68+
17 | class E(NamedTuple, Protocol): ...
69+
| ^^^^^^^^
70+
|
71+
info: rule `invalid-named-tuple` is enabled by default
72+
73+
```

crates/ty_python_semantic/src/types/class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ impl CodeGeneratorKind {
227227
code_generator_of_class(db, class)
228228
}
229229

230-
fn matches(self, db: &dyn Db, class: ClassLiteral<'_>) -> bool {
230+
pub(super) fn matches(self, db: &dyn Db, class: ClassLiteral<'_>) -> bool {
231231
CodeGeneratorKind::from_class(db, class) == Some(self)
232232
}
233233
}

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
5757
registry.register_lint(&INVALID_OVERLOAD);
5858
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
5959
registry.register_lint(&INVALID_PROTOCOL);
60+
registry.register_lint(&INVALID_NAMED_TUPLE);
6061
registry.register_lint(&INVALID_RAISE);
6162
registry.register_lint(&INVALID_SUPER_ARGUMENT);
6263
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
@@ -448,6 +449,39 @@ declare_lint! {
448449
}
449450
}
450451

452+
declare_lint! {
453+
/// ## What it does
454+
/// Checks for invalidly defined `NamedTuple` classes.
455+
///
456+
/// ## Why is this bad?
457+
/// An invalidly defined `NamedTuple` class may lead to the type checker
458+
/// inferring unexpected things. It may also lead to `TypeError`s at runtime.
459+
///
460+
/// ## Examples
461+
/// A class definition cannot combine `NamedTuple` with other base classes
462+
/// in multiple inheritance; doing so raises a `TypeError` at runtime. The sole
463+
/// exception to this rule is `Generic[]`, which can be used alongside `NamedTuple`
464+
/// in a class's bases list.
465+
///
466+
/// ```pycon
467+
/// >>> from typing import NamedTuple
468+
/// >>> class Foo(NamedTuple, object): ...
469+
/// ...
470+
/// Traceback (most recent call last):
471+
/// File "<python-input-1>", line 1, in <module>
472+
/// class Foo(NamedTuple, object): ...
473+
/// File "/python3.13/typing.py", line 2998, in __new__
474+
/// raise TypeError(
475+
/// 'can only inherit from a NamedTuple type and Generic')
476+
/// TypeError: can only inherit from a NamedTuple type and Generic
477+
/// ```
478+
pub(crate) static INVALID_NAMED_TUPLE = {
479+
summary: "detects invalid `NamedTuple` class definitions",
480+
status: LintStatus::preview("1.0.0"),
481+
default_level: Level::Error,
482+
}
483+
}
484+
451485
declare_lint! {
452486
/// ## What it does
453487
/// Checks for classes with an inconsistent [method resolution order] (MRO).

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,17 @@ use crate::types::diagnostic::{
9595
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
9696
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
9797
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
98-
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_PARAMETER_DEFAULT,
99-
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
100-
IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
101-
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
102-
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
103-
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
104-
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
105-
report_invalid_assignment, report_invalid_attribute_assignment,
106-
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
107-
report_invalid_return_type, report_possibly_unbound_attribute,
98+
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE,
99+
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
100+
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
101+
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
102+
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
103+
report_implicit_return_type, report_instance_layout_conflict,
104+
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
105+
report_invalid_arguments_to_callable, report_invalid_assignment,
106+
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
107+
report_invalid_key_on_typed_dict, report_invalid_return_type,
108+
report_possibly_unbound_attribute,
108109
};
109110
use crate::types::enums::is_enum_class;
110111
use crate::types::function::{
@@ -1110,13 +1111,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
11101111
}
11111112

11121113
let is_protocol = class.is_protocol(self.db());
1114+
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class);
11131115
let mut solid_bases = IncompatibleBases::default();
11141116

11151117
// (2) Iterate through the class's explicit bases to check for various possible errors:
11161118
// - Check for inheritance from plain `Generic`,
11171119
// - Check for inheritance from a `@final` classes
11181120
// - If the class is a protocol class: check for inheritance from a non-protocol class
1121+
// - If the class is a NamedTuple class: check for multiple inheritance that isn't `Generic[]`
11191122
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
1123+
if is_named_tuple
1124+
&& !matches!(
1125+
base_class,
1126+
Type::SpecialForm(SpecialFormType::NamedTuple)
1127+
| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_))
1128+
)
1129+
{
1130+
if let Some(builder) = self
1131+
.context
1132+
.report_lint(&INVALID_NAMED_TUPLE, &class_node.bases()[i])
1133+
{
1134+
builder.into_diagnostic(format_args!(
1135+
"NamedTuple class `{}` cannot use multiple inheritance except with `Generic[]`",
1136+
class.name(self.db()),
1137+
));
1138+
}
1139+
}
1140+
11201141
let base_class = match base_class {
11211142
Type::SpecialForm(SpecialFormType::Generic) => {
11221143
if let Some(builder) = self

ty.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)