mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Apply class decorators via try_call() (#22375)
## Summary Decorators are now called with the class as an argument, and the return type becomes the class's type. This mirrors how function decorators already work. Closes https://github.com/astral-sh/ty/issues/2313.
This commit is contained in:
@@ -234,3 +234,38 @@ def takes_no_argument() -> str:
|
||||
@takes_no_argument
|
||||
def g(x): ...
|
||||
```
|
||||
|
||||
## Class decorators
|
||||
|
||||
Class decorator calls are validated, emitting diagnostics for invalid arguments:
|
||||
|
||||
```py
|
||||
def takes_int(x: int) -> int:
|
||||
return x
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
@takes_int
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
Using `None` as a decorator is an error:
|
||||
|
||||
```py
|
||||
# error: [call-non-callable]
|
||||
@None
|
||||
class Bar: ...
|
||||
```
|
||||
|
||||
A decorator can enforce type constraints on the class being decorated:
|
||||
|
||||
```py
|
||||
def decorator(cls: type[int]) -> type[int]:
|
||||
return cls
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
@decorator
|
||||
class Baz: ...
|
||||
|
||||
# TODO: the revealed type should ideally be `type[int]` (the decorator's return type)
|
||||
reveal_type(Baz) # revealed: <class 'Baz'>
|
||||
```
|
||||
|
||||
@@ -2797,6 +2797,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
body: _,
|
||||
} = class_node;
|
||||
|
||||
let mut decorator_types_and_nodes: Vec<(Type<'db>, &ast::Decorator)> =
|
||||
Vec::with_capacity(decorator_list.len());
|
||||
let mut deprecated = None;
|
||||
let mut type_check_only = false;
|
||||
let mut dataclass_params = None;
|
||||
@@ -2831,6 +2833,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip identity decorators to avoid salsa cycles on typeshed.
|
||||
if decorator_ty.as_function_literal().is_some_and(|function| {
|
||||
matches!(
|
||||
function.known(self.db()),
|
||||
Some(
|
||||
KnownFunction::Final
|
||||
| KnownFunction::DisjointBase
|
||||
| KnownFunction::RuntimeCheckable
|
||||
)
|
||||
)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Type::FunctionLiteral(f) = decorator_ty {
|
||||
// We do not yet detect or flag `@dataclass_transform` applied to more than one
|
||||
// overload, or an overload and the implementation both. Nevertheless, this is not
|
||||
@@ -2852,6 +2868,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
dataclass_transformer_params = Some(params);
|
||||
continue;
|
||||
}
|
||||
|
||||
decorator_types_and_nodes.push((decorator_ty, decorator));
|
||||
}
|
||||
|
||||
let body_scope = self
|
||||
@@ -2868,7 +2886,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
)
|
||||
};
|
||||
|
||||
let ty = match (maybe_known_class, &*name.id) {
|
||||
let inferred_ty = match (maybe_known_class, &*name.id) {
|
||||
(None, "NamedTuple") if in_typing_module() => {
|
||||
Type::SpecialForm(SpecialFormType::NamedTuple)
|
||||
}
|
||||
@@ -2885,10 +2903,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
)),
|
||||
};
|
||||
|
||||
// Validate decorator calls (but don't use return types yet).
|
||||
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
|
||||
if let Err(CallError(_, bindings)) =
|
||||
decorator_ty.try_call(self.db(), &CallArguments::positional([inferred_ty]))
|
||||
{
|
||||
bindings.report_diagnostics(&self.context, (*decorator_node).into());
|
||||
}
|
||||
}
|
||||
|
||||
self.add_declaration_with_binding(
|
||||
class_node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::are_the_same_type(ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
|
||||
);
|
||||
|
||||
// if there are type parameters, then the keywords and bases are within that scope
|
||||
|
||||
Reference in New Issue
Block a user