From a8f7ccf2ca0959046ea53f44e35282920fdd58b8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 19 Nov 2025 19:27:12 +0000 Subject: [PATCH] [ty] Improve diagnostics when `NotImplemented` is called (#21523) ## Summary Fixes https://github.com/astral-sh/ty/issues/1571. I realised I was overcomplicating things when I described what we should do in that issue description. The simplest thing to do here is just to special-case call expressions and short-circuit the call-binding machinery entirely if we see it's `NotImplemented` being called. It doesn't really matter if the subdiagnostic doesn't fire when a union is called and one element of the union is `NotImplemented` -- the subdiagnostic doesn't need to be exhaustive; it's just to help people in some common cases. ## Test Plan Added snapshots --- .../resources/mdtest/call/builtins.md | 5 +- ..._builtin_`NotImpl…_(ac366391ebdec9c0).snap | 47 +++++++++++++++++++ .../src/types/infer/builder.rs | 18 +++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/builtins.md_-_Calling_builtins_-_The_builtin_`NotImpl…_(ac366391ebdec9c0).snap diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 742d275e3d..bac85f49d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -200,6 +200,9 @@ isinstance("", t.Any) # error: [invalid-argument-type] ## The builtin `NotImplemented` constant is not callable + + ```py -NotImplemented() # error: [call-non-callable] +raise NotImplemented() # error: [call-non-callable] +raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/builtins.md_-_Calling_builtins_-_The_builtin_`NotImpl…_(ac366391ebdec9c0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/builtins.md_-_Calling_builtins_-_The_builtin_`NotImpl…_(ac366391ebdec9c0).snap new file mode 100644 index 0000000000..0e6ea4d536 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/builtins.md_-_Calling_builtins_-_The_builtin_`NotImpl…_(ac366391ebdec9c0).snap @@ -0,0 +1,47 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: builtins.md - Calling builtins - The builtin `NotImplemented` constant is not callable +mdtest path: crates/ty_python_semantic/resources/mdtest/call/builtins.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | raise NotImplemented() # error: [call-non-callable] +2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable] +``` + +# Diagnostics + +``` +error[call-non-callable]: `NotImplemented` is not callable + --> src/mdtest_snippet.py:1:7 + | +1 | raise NotImplemented() # error: [call-non-callable] + | --------------^^ + | | + | Did you mean `NotImplementedError`? +2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable] + | +info: rule `call-non-callable` is enabled by default + +``` + +``` +error[call-non-callable]: `NotImplemented` is not callable + --> src/mdtest_snippet.py:2:7 + | +1 | raise NotImplemented() # error: [call-non-callable] +2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable] + | --------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | + | Did you mean `NotImplementedError`? + | +info: rule `call-non-callable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 85c645d37a..3afbd10858 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7912,6 +7912,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty }); + if callable_type.is_notimplemented(self.db()) { + if let Some(builder) = self + .context + .report_lint(&CALL_NON_CALLABLE, call_expression) + { + let mut diagnostic = builder.into_diagnostic("`NotImplemented` is not callable"); + diagnostic.annotate( + self.context + .secondary(&**func) + .message("Did you mean `NotImplementedError`?"), + ); + diagnostic.set_concise_message( + "`NotImplemented` is not callable - did you mean `NotImplementedError`?", + ); + } + return Type::unknown(); + } + // Special handling for `TypedDict` method calls if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { let value_type = self.expression_type(value);