From 139149f87b4ad3afb1fe2f4b97af280449fdd741 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 24 Dec 2025 19:18:51 +0000 Subject: [PATCH] [ty] Improve diagnostic when `callable` is used in a type expression instead of `collections.abc.Callable` or `typing.Callable` (#22180) --- .../resources/mdtest/annotations/invalid.md | 9 ++++ ..._Special-cased_diagno…_(a4b698196d337a3f).snap | 49 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 25 +++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Special-cased_diagno…_(a4b698196d337a3f).snap diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index de5f16dcbb..32626dc519 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -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 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Special-cased_diagno…_(a4b698196d337a3f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Special-cased_diagno…_(a4b698196d337a3f).snap new file mode 100644 index 0000000000..f6869c0174 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Special-cased_diagno…_(a4b698196d337a3f).snap @@ -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 + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 268a9af272..298c35f3c2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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`?"); } } }