Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 91 additions & 61 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

21 changes: 17 additions & 4 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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

```
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
27 changes: 27 additions & 0 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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).
Expand Down
41 changes: 31 additions & 10 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions ty.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading