[ty] Improve diagnostic when callable is used in a type expression instead of collections.abc.Callable or typing.Callable (#22180)

This commit is contained in:
Alex Waygood
2025-12-24 19:18:51 +00:00
committed by GitHub
parent 2de4464e92
commit 139149f87b
3 changed files with 81 additions and 2 deletions

View File

@@ -266,3 +266,12 @@ def _(
) -> (int, str): # error: [invalid-type-form]
return x
```
### Special-cased diagnostic for `callable` used in a type expression
```py
# error: [invalid-type-form]
# error: [invalid-type-form]
def decorator(fn: callable) -> callable:
return fn
```

View File

@@ -0,0 +1,49 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Special-cased diagnostic for `callable` used in a type expression
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---
# Python source files
## mdtest_snippet.py
```
1 | # error: [invalid-type-form]
2 | # error: [invalid-type-form]
3 | def decorator(fn: callable) -> callable:
4 | return fn
```
# Diagnostics
```
error[invalid-type-form]: Function `callable` is not valid in a type expression
--> src/mdtest_snippet.py:3:19
|
1 | # error: [invalid-type-form]
2 | # error: [invalid-type-form]
3 | def decorator(fn: callable) -> callable:
| ^^^^^^^^ Did you mean `collections.abc.Callable`?
4 | return fn
|
info: rule `invalid-type-form` is enabled by default
```
```
error[invalid-type-form]: Function `callable` is not valid in a type expression
--> src/mdtest_snippet.py:3:32
|
1 | # error: [invalid-type-form]
2 | # error: [invalid-type-form]
3 | def decorator(fn: callable) -> callable:
| ^^^^^^^^ Did you mean `collections.abc.Callable`?
4 | return fn
|
info: rule `invalid-type-form` is enabled by default
```

View File

@@ -36,8 +36,8 @@ pub(crate) use self::signatures::{CallableSignature, Signature};
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic;
use crate::place::{
Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, imported_symbol,
known_module_symbol,
Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, builtins_module_scope,
imported_symbol, known_module_symbol,
};
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::ScopedPlaceId;
@@ -9459,6 +9459,13 @@ impl<'db> InvalidTypeExpression<'db> {
"Type qualifier `{qualifier}` is not allowed in type expressions \
(only in annotation expressions, and only with exactly one argument)",
),
InvalidTypeExpression::InvalidType(Type::FunctionLiteral(function), _) => {
write!(
f,
"Function `{function}` is not valid in a type expression",
function = function.name(self.db)
)
}
InvalidTypeExpression::InvalidType(Type::ModuleLiteral(module), _) => write!(
f,
"Module `{module}` is not valid in a type expression",
@@ -9520,6 +9527,20 @@ impl<'db> InvalidTypeExpression<'db> {
"You might have meant to use a concrete TypedDict \
or `collections.abc.Mapping[str, object]`",
);
// It would be nice if we could register `builtins.callable` as a known function,
// but currently doing this would require reimplementing the signature "manually"
// in `Type::bindings()`, which isn't worth it given that we have no other special
// casing for this function.
} else if let InvalidTypeExpression::InvalidType(Type::FunctionLiteral(function), _) = self
&& function.name(db) == "callable"
&& let function_body_scope = function.literal(db).last_definition(db).body_scope(db)
&& function_body_scope
.scope(db)
.parent()
.map(|parent| parent.to_scope_id(db, function_body_scope.file(db)))
== builtins_module_scope(db)
{
diagnostic.set_primary_message("Did you mean `collections.abc.Callable`?");
}
}
}