[red-knot] More precise inference for classes with non-class metaclasses (#15138)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo build (release) (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
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 shear (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (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 / python package (push) Waiting to run
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 / benchmarks (push) Blocked by required conditions

## Summary

Resolves #14208.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
InSync 2025-01-09 07:34:04 +07:00 committed by GitHub
parent 5f5eb7c0dd
commit 21aa12a073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 132 additions and 7 deletions

View file

@ -170,8 +170,35 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
# TODO: Should be `int`
reveal_type(A) # revealed: Literal[A]
reveal_type(A.__class__) # revealed: type[int]
def _(n: int):
# error: [invalid-metaclass]
class B(metaclass=n): ...
# TODO: Should be `Unknown`
reveal_type(B) # revealed: Literal[B]
reveal_type(B.__class__) # revealed: type[Unknown]
def _(flag: bool):
m = f if flag else 42
# error: [invalid-metaclass]
class C(metaclass=m): ...
# TODO: Should be `int | Unknown`
reveal_type(C) # revealed: Literal[C]
reveal_type(C.__class__) # revealed: type[Unknown]
class SignatureMismatch: ...
# TODO: Emit a diagnostic
class D(metaclass=SignatureMismatch): ...
# TODO: Should be `Unknown`
reveal_type(D) # revealed: Literal[D]
# TODO: Should be `type[Unknown]`
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
```
## Cyclic

View file

@ -3610,10 +3610,62 @@ impl<'db> Class<'db> {
explicit_metaclass_of: class_metaclass_was_from,
}
} else {
// TODO: If the metaclass is not a class, we should verify that it's a callable
// which accepts the same arguments as `type.__new__` (otherwise error), and return
// the meta-type of its return type. (And validate that is a class type?)
return Ok(todo_type!("metaclass not a class"));
let name = Type::string_literal(db, self.name(db));
let bases = TupleType::from_elements(db, self.explicit_bases(db));
// TODO: Should be `dict[str, Any]`
let namespace = KnownClass::Dict.to_instance(db);
// TODO: Other keyword arguments?
let arguments = CallArguments::positional([name, bases, namespace]);
let return_ty_result = match metaclass.call(db, &arguments) {
CallOutcome::NotCallable { not_callable_ty } => Err(MetaclassError {
kind: MetaclassErrorKind::NotCallable(not_callable_ty),
}),
CallOutcome::Union {
outcomes,
called_ty,
} => {
let mut partly_not_callable = false;
let return_ty = outcomes
.iter()
.fold(None, |acc, outcome| {
let ty = outcome.return_ty(db);
match (acc, ty) {
(acc, None) => {
partly_not_callable = true;
acc
}
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), Some(ty)) => Some(builder.add(ty)),
}
})
.map(UnionBuilder::build);
if partly_not_callable {
Err(MetaclassError {
kind: MetaclassErrorKind::PartlyNotCallable(called_ty),
})
} else {
Ok(return_ty.unwrap_or(Type::Unknown))
}
}
CallOutcome::PossiblyUnboundDunderCall { called_ty, .. } => Err(MetaclassError {
kind: MetaclassErrorKind::PartlyNotCallable(called_ty),
}),
// TODO we should also check for binding errors that would indicate the metaclass
// does not accept the right arguments
CallOutcome::Callable { binding }
| CallOutcome::RevealType { binding, .. }
| CallOutcome::StaticAssertionError { binding, .. } => Ok(binding.return_ty()),
};
return return_ty_result.map(|ty| ty.to_meta_type(db));
};
// Reconcile all base classes' metaclasses with the candidate metaclass.
@ -3818,6 +3870,10 @@ pub(super) enum MetaclassErrorKind<'db> {
/// inferred metaclass of a base class. This helps us give better error messages in diagnostics.
candidate1_is_base_class: bool,
},
/// The metaclass is not callable
NotCallable(Type<'db>),
/// The metaclass is of a union type whose some members are not callable
PartlyNotCallable(Type<'db>),
}
#[salsa::interned]

View file

@ -36,6 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_TYPE_FORM);
@ -290,6 +291,7 @@ declare_lint! {
}
declare_lint! {
/// ## What it does
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
@ -324,6 +326,33 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for arguments to `metaclass=` that are invalid.
///
/// ## Why is this bad?
/// Python allows arbitrary expressions to be used as the argument to `metaclass=`.
/// These expressions, however, need to be callable and accept the same arguments
/// as `type.__new__`.
///
/// ## Example
///
/// ```python
/// def f(): ...
///
/// # TypeError: f() takes 0 positional arguments but 3 were given
/// class B(metaclass=f): ...
/// ```
///
/// ## References
/// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
pub(crate) static INVALID_METACLASS = {
summary: "detects invalid `metaclass=` arguments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.

View file

@ -77,7 +77,7 @@ use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_non_subscriptable,
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
SUBCLASS_OF_FINAL_CLASS,
INVALID_METACLASS, SUBCLASS_OF_FINAL_CLASS,
};
use super::slots::check_class_slots;
use super::string_annotation::{
@ -639,6 +639,19 @@ impl<'db> TypeInferenceBuilder<'db> {
// (4) Check that the class's metaclass can be determined without error.
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
match metaclass_error.reason() {
MetaclassErrorKind::NotCallable(ty) => self.context.report_lint(
&INVALID_METACLASS,
class_node.into(),
format_args!("Metaclass type `{}` is not callable", ty.display(self.db())),
),
MetaclassErrorKind::PartlyNotCallable(ty) => self.context.report_lint(
&INVALID_METACLASS,
class_node.into(),
format_args!(
"Metaclass type `{}` is partly not callable",
ty.display(self.db())
),
),
MetaclassErrorKind::Conflict {
candidate1:
MetaclassCandidate {