[ty] Split invalid-base error code into two error codes (#18245)
Some checks are pending
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / benchmarks (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-05-21 18:02:39 -04:00 committed by GitHub
parent 02394b8049
commit cb04343b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 568 additions and 130 deletions

View file

@ -173,7 +173,7 @@ if hasattr(DoesNotExist, "__mro__"):
if not isinstance(DoesNotExist, type):
reveal_type(DoesNotExist) # revealed: Unknown & ~type
class Foo(DoesNotExist): ... # error: [invalid-base]
class Foo(DoesNotExist): ... # error: [unsupported-base]
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
@ -232,11 +232,15 @@ reveal_type(AA.__mro__) # revealed: tuple[<class 'AA'>, <class 'Z'>, Unknown, <
## `__bases__` includes a `Union`
<!-- snapshot-diagnostics -->
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py
from typing_extensions import reveal_type
def returns_bool() -> bool:
return True
@ -250,7 +254,7 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
class Foo(x): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@ -259,8 +263,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'obje
## `__bases__` is a union of a dynamic type and valid bases
If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
types *would be* valid class bases, we do not emit an `invalid-base` diagnostic and use the dynamic
type as a base to prevent further downstream errors.
types *would be* valid class bases, we do not emit an `invalid-base` or `unsupported-base`
diagnostic, and we use the dynamic type as a base to prevent further downstream errors.
```py
from typing import Any
@ -299,8 +303,8 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
reveal_type(y) # revealed: <class 'C'> | <class 'D'>
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 14 [invalid-base] "Invalid class base with type `<class 'C'> | <class 'D'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
# error: 14 [unsupported-base] "Unsupported class base with type `<class 'C'> | <class 'D'>`"
class Foo(x, y): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@ -321,7 +325,7 @@ if returns_bool():
else:
foo = object
# error: 21 [invalid-base] "Invalid class base with type `<class 'Y'> | <class 'object'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 21 [unsupported-base] "Unsupported class base with type `<class 'Y'> | <class 'object'>`"
class PossibleError(foo, X): ...
reveal_type(PossibleError.__mro__) # revealed: tuple[<class 'PossibleError'>, Unknown, <class 'object'>]
@ -339,12 +343,47 @@ else:
# revealed: tuple[<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>] | tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
reveal_type(B.__mro__)
# error: 12 [invalid-base] "Invalid class base with type `<class 'B'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
```
## `__bases__` lists that include objects that are not instances of `type`
<!-- snapshot-diagnostics -->
```py
class Foo(2): ... # error: [invalid-base]
```
A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an
exception at runtime, so we issue `unsupported-base` rather than `invalid-base`:
```py
class Foo:
def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
return ()
class Bar(Foo()): ... # error: [unsupported-base]
```
But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than
`unsupported-base`:
```py
class Bad1:
def __mro_entries__(self, bases, extra_arg):
return ()
class Bad2:
def __mro_entries__(self, bases) -> int:
return 42
class BadSub1(Bad1()): ... # error: [invalid-base]
class BadSub2(Bad2()): ... # error: [invalid-base]
```
## `__bases__` lists with duplicate bases
<!-- snapshot-diagnostics -->

View file

@ -0,0 +1,78 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def returns_bool() -> bool:
4 | return True
5 |
6 | class A: ...
7 | class B: ...
8 |
9 | if returns_bool():
10 | x = A
11 | else:
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
# Diagnostics
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:13
|
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
| ^ `<class 'A'> | <class 'B'>`
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
|
```
```
warning[unsupported-base]: Unsupported class base with type `<class 'A'> | <class 'B'>`
--> src/mdtest_snippet.py:17:11
|
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
| ^
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
info: ty cannot resolve a consistent MRO for class `Foo` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:19:13
|
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
| ^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
```

View file

@ -0,0 +1,97 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Foo(2): ... # error: [invalid-base]
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
9 | return ()
10 |
11 | class Bad2:
12 | def __mro_entries__(self, bases) -> int:
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid class base with type `Literal[2]`
--> src/mdtest_snippet.py:1:11
|
1 | class Foo(2): ... # error: [invalid-base]
| ^
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
|
info: Definition of class `Foo` will raise `TypeError` at runtime
info: rule `invalid-base` is enabled by default
```
```
warning[unsupported-base]: Unsupported class base with type `Foo`
--> src/mdtest_snippet.py:6:11
|
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
| ^^^^^
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
|
info: ty cannot resolve a consistent MRO for class `Bar` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad1`
--> src/mdtest_snippet.py:15:15
|
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
| ^^^^^^
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
|
info: Definition of class `BadSub1` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments
info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`
info: rule `invalid-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad2`
--> src/mdtest_snippet.py:16:15
|
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
| ^^^^^^
|
info: Definition of class `BadSub2` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types
info: rule `invalid-base` is enabled by default
```

View file

@ -164,8 +164,12 @@ impl<'db> ClassBase<'db> {
}
}
Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`?
Type::PropertyInstance(_) => None,
Type::Never
// This likely means that we're in unreachable code,
// in which case we want to treat `Never` in a forgiving way and silence diagnostics
Type::Never => Some(ClassBase::unknown()),
Type::PropertyInstance(_)
| Type::BooleanLiteral(_)
| Type::FunctionLiteral(_)
| Type::Callable(..)

View file

@ -1,6 +1,7 @@
use super::call::CallErrorKind;
use super::context::InferContext;
use super::mro::DuplicateBaseError;
use super::{ClassBase, ClassLiteral, KnownClass};
use super::{CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass};
use crate::db::Db;
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::suppression::FileSuppressionId;
@ -70,6 +71,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&UNSUPPORTED_BASE);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
@ -451,14 +453,56 @@ declare_lint! {
}
declare_lint! {
/// TODO #14889
/// ## What it does
/// Checks for class definitions that have bases which are not instances of `type`.
///
/// ## Why is this bad?
/// Class definitions with bases like this will lead to `TypeError` being raised at runtime.
///
/// ## Examples
/// ```python
/// class A(42): ... # error: [invalid-base]
/// ```
pub(crate) static INVALID_BASE = {
summary: "detects invalid bases in class definitions",
summary: "detects class bases that will cause the class definition to raise an exception at runtime",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for class definitions that have bases which are unsupported by ty.
///
/// ## Why is this bad?
/// If a class has a base that is an instance of a complex type such as a union type,
/// ty will not be able to resolve the [method resolution order] (MRO) for the class.
/// This will lead to an inferior understanding of your codebase and unpredictable
/// type-checking behavior.
///
/// ## Examples
/// ```python
/// import datetime
///
/// class A: ...
/// class B: ...
///
/// if datetime.date.today().weekday() != 6:
/// C = A
/// else:
/// C = B
///
/// class D(C): ... # error: [unsupported-base]
/// ```
///
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
pub(crate) static UNSUPPORTED_BASE = {
summary: "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for expressions used in `with` statements
@ -1976,3 +2020,132 @@ pub(crate) fn report_duplicate_bases(
diagnostic.sub(sub_diagnostic);
}
pub(crate) fn report_invalid_or_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
class: ClassLiteral,
) {
let db = context.db();
let instance_of_type = KnownClass::Type.to_instance(db);
if base_type.is_assignable_to(db, instance_of_type) {
report_unsupported_base(context, base_node, base_type, class);
return;
}
let tuple_of_types = KnownClass::Tuple.to_specialized_instance(db, [instance_of_type]);
let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| {
diagnostic.info(
"An instance type is only a valid class base \
if it has a valid `__mro_entries__` method",
);
};
match base_type.try_call_dunder(
db,
"__mro_entries__",
CallArgumentTypes::positional([tuple_of_types]),
) {
Ok(ret) => {
if ret.return_type(db).is_assignable_to(db, tuple_of_types) {
report_unsupported_base(context, base_node, base_type, class);
} else {
let Some(mut diagnostic) =
report_invalid_base(context, base_node, base_type, class)
else {
return;
};
explain_mro_entries(&mut diagnostic);
diagnostic.info(format_args!(
"Type `{}` has an `__mro_entries__` method, but it does not return a tuple of types",
base_type.display(db)
));
}
}
Err(mro_entries_call_error) => {
let Some(mut diagnostic) = report_invalid_base(context, base_node, base_type, class)
else {
return;
};
match mro_entries_call_error {
CallDunderError::MethodNotAvailable => {}
CallDunderError::PossiblyUnbound(_) => {
explain_mro_entries(&mut diagnostic);
diagnostic.info(format_args!(
"Type `{}` has an `__mro_entries__` attribute, but it is possibly unbound",
base_type.display(db)
));
}
CallDunderError::CallError(CallErrorKind::NotCallable, _) => {
explain_mro_entries(&mut diagnostic);
diagnostic.info(format_args!(
"Type `{}` has an `__mro_entries__` attribute, but it is not callable",
base_type.display(db)
));
}
CallDunderError::CallError(CallErrorKind::BindingError, _) => {
explain_mro_entries(&mut diagnostic);
diagnostic.info(format_args!(
"Type `{}` has an `__mro_entries__` method, \
but it cannot be called with the expected arguments",
base_type.display(db)
));
diagnostic.info(
"Expected a signature at least as permissive as \
`def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`"
);
}
CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => {
explain_mro_entries(&mut diagnostic);
diagnostic.info(format_args!(
"Type `{}` has an `__mro_entries__` method, \
but it may not be callable",
base_type.display(db)
));
}
}
}
}
}
fn report_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
class: ClassLiteral,
) {
let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else {
return;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
"Unsupported class base with type `{}`",
base_type.display(context.db())
));
diagnostic.info(format_args!(
"ty cannot resolve a consistent MRO for class `{}` due to this base",
class.name(context.db())
));
diagnostic.info("Only class objects or `Any` are supported as class bases");
}
fn report_invalid_base<'ctx, 'db>(
context: &'ctx InferContext<'db>,
base_node: &ast::Expr,
base_type: Type<'db>,
class: ClassLiteral<'db>,
) -> Option<LintDiagnosticGuard<'ctx, 'db>> {
let builder = context.report_lint(&INVALID_BASE, base_node)?;
let mut diagnostic = builder.into_diagnostic(format_args!(
"Invalid class base with type `{}`",
base_type.display(context.db())
));
diagnostic.info(format_args!(
"Definition of class `{}` will raise `TypeError` at runtime",
class.name(context.db())
));
Some(diagnostic)
}

View file

@ -102,8 +102,9 @@ use super::diagnostic::{
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, report_attempted_protocol_instantiation,
report_bad_argument_to_get_protocol_members, report_duplicate_bases,
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_type_checking_constant,
report_non_subscriptable, report_possibly_unresolved_reference,
report_invalid_exception_raised, report_invalid_or_unsupported_base,
report_invalid_type_checking_constant, report_non_subscriptable,
report_possibly_unresolved_reference,
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
report_unresolved_reference,
};
@ -892,63 +893,51 @@ impl<'db> TypeInferenceBuilder<'db> {
// (3) Check that the class's MRO is resolvable
match class.try_mro(self.db(), None) {
Err(mro_error) => {
match mro_error.reason() {
MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases();
for duplicate in duplicates {
report_duplicate_bases(&self.context, class, duplicate, base_nodes);
}
}
MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases();
for (index, base_ty) in bases {
if base_ty.is_never() {
// A class base of type `Never` can appear in unreachable code. It
// does not indicate a problem, since the actual construction of the
// class will never happen.
continue;
}
let Some(builder) =
self.context.report_lint(&INVALID_BASE, &base_nodes[*index])
else {
continue;
};
builder.into_diagnostic(format_args!(
"Invalid class base with type `{}` \
(all bases must be a class, `Any`, `Unknown` or `Todo`)",
base_ty.display(self.db())
));
}
}
MroErrorKind::UnresolvableMro { bases_list } => {
if let Some(builder) =
self.context.report_lint(&INCONSISTENT_MRO, class_node)
{
builder.into_diagnostic(format_args!(
"Cannot create a consistent method resolution order (MRO) \
for class `{}` with bases list `[{}]`",
class.name(self.db()),
bases_list
.iter()
.map(|base| base.display(self.db()))
.join(", ")
));
}
}
MroErrorKind::InheritanceCycle => {
if let Some(builder) = self
.context
.report_lint(&CYCLIC_CLASS_DEFINITION, class_node)
{
builder.into_diagnostic(format_args!(
"Cyclic definition of `{}` (class cannot inherit from itself)",
class.name(self.db())
));
}
Err(mro_error) => match mro_error.reason() {
MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases();
for duplicate in duplicates {
report_duplicate_bases(&self.context, class, duplicate, base_nodes);
}
}
}
MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases();
for (index, base_ty) in bases {
report_invalid_or_unsupported_base(
&self.context,
&base_nodes[*index],
*base_ty,
class,
);
}
}
MroErrorKind::UnresolvableMro { bases_list } => {
if let Some(builder) =
self.context.report_lint(&INCONSISTENT_MRO, class_node)
{
builder.into_diagnostic(format_args!(
"Cannot create a consistent method resolution order (MRO) \
for class `{}` with bases list `[{}]`",
class.name(self.db()),
bases_list
.iter()
.map(|base| base.display(self.db()))
.join(", ")
));
}
}
MroErrorKind::InheritanceCycle => {
if let Some(builder) = self
.context
.report_lint(&CYCLIC_CLASS_DEFINITION, class_node)
{
builder.into_diagnostic(format_args!(
"Cyclic definition of `{}` (class cannot inherit from itself)",
class.name(self.db())
));
}
}
},
Ok(_) => check_class_slots(&self.context, class, class_node),
}

View file

@ -1,7 +1,8 @@
use std::collections::VecDeque;
use std::ops::Deref;
use rustc_hash::FxHashMap;
use indexmap::IndexMap;
use rustc_hash::FxBuildHasher;
use crate::Db;
use crate::types::class_base::ClassBase;
@ -157,8 +158,8 @@ impl<'db> Mro<'db> {
let mut duplicate_dynamic_bases = false;
let duplicate_bases: Vec<DuplicateBaseError<'db>> = {
let mut base_to_indices: FxHashMap<ClassBase<'db>, Vec<usize>> =
FxHashMap::default();
let mut base_to_indices: IndexMap<ClassBase<'db>, Vec<usize>, FxBuildHasher> =
IndexMap::default();
for (index, base) in valid_bases.iter().enumerate() {
base_to_indices.entry(*base).or_default().push(index);