diff --git a/crates/ty_python_semantic/resources/mdtest/exception/basic.md b/crates/ty_python_semantic/resources/mdtest/exception/basic.md index 72b26d89f5..0d65b9b849 100644 --- a/crates/ty_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/basic.md @@ -116,17 +116,18 @@ def silence2[T: ( ## Invalid exception handlers + + ```py try: pass -# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +# error: [invalid-exception-caught] except 3 as e: reveal_type(e) # revealed: Unknown try: pass -# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" -# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +# error: [invalid-exception-caught] except (ValueError, OSError, "foo", b"bar") as e: reveal_type(e) # revealed: ValueError | OSError | Unknown @@ -278,3 +279,20 @@ def f(x: type[Exception]): # error: [possibly-unresolved-reference] reveal_type(e) # revealed: None ``` + +## Special-cased diagnostics for `NotImplemented` + + + +```py +try: + # error: [invalid-raise] + # error: [invalid-raise] + raise NotImplemented from NotImplemented +# error: [invalid-exception-caught] +except NotImplemented: + pass +# error: [invalid-exception-caught] +except (TypeError, NotImplemented): + pass +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha…_(d394c561bdd35078).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha…_(d394c561bdd35078).snap new file mode 100644 index 0000000000..3734ba3756 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha…_(d394c561bdd35078).snap @@ -0,0 +1,146 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Exception Handling - Invalid exception handlers +mdtest path: crates/ty_python_semantic/resources/mdtest/exception/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | try: + 2 | pass + 3 | # error: [invalid-exception-caught] + 4 | except 3 as e: + 5 | reveal_type(e) # revealed: Unknown + 6 | + 7 | try: + 8 | pass + 9 | # error: [invalid-exception-caught] +10 | except (ValueError, OSError, "foo", b"bar") as e: +11 | reveal_type(e) # revealed: ValueError | OSError | Unknown +12 | +13 | def foo( +14 | x: type[str], +15 | y: tuple[type[OSError], type[RuntimeError], int], +16 | z: tuple[type[str], ...], +17 | ): +18 | try: +19 | help() +20 | # error: [invalid-exception-caught] +21 | except x as e: +22 | reveal_type(e) # revealed: Unknown +23 | # error: [invalid-exception-caught] +24 | except y as f: +25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown +26 | # error: [invalid-exception-caught] +27 | except z as g: +28 | reveal_type(g) # revealed: Unknown +29 | +30 | try: +31 | {}.get("foo") +32 | # error: [invalid-exception-caught] +33 | except int: +34 | pass +``` + +# Diagnostics + +``` +error[invalid-exception-caught]: Invalid object caught in an exception handler + --> src/mdtest_snippet.py:4:8 + | +2 | pass +3 | # error: [invalid-exception-caught] +4 | except 3 as e: + | ^ Object has type `Literal[3]` +5 | reveal_type(e) # revealed: Unknown + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid tuple caught in an exception handler + --> src/mdtest_snippet.py:10:8 + | + 8 | pass + 9 | # error: [invalid-exception-caught] +10 | except (ValueError, OSError, "foo", b"bar") as e: + | ^^^^^^^^^^^^^^^^^^^^^^-----^^------^ + | | | + | | Invalid element of type `Literal[b"bar"]` + | Invalid element of type `Literal["foo"]` +11 | reveal_type(e) # revealed: ValueError | OSError | Unknown + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid object caught in an exception handler + --> src/mdtest_snippet.py:21:12 + | +19 | help() +20 | # error: [invalid-exception-caught] +21 | except x as e: + | ^ Object has type `type[str]` +22 | reveal_type(e) # revealed: Unknown +23 | # error: [invalid-exception-caught] + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid tuple caught in an exception handler + --> src/mdtest_snippet.py:24:12 + | +22 | reveal_type(e) # revealed: Unknown +23 | # error: [invalid-exception-caught] +24 | except y as f: + | ^ Object has type `tuple[type[OSError], type[RuntimeError], int]` +25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown +26 | # error: [invalid-exception-caught] + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid tuple caught in an exception handler + --> src/mdtest_snippet.py:27:12 + | +25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown +26 | # error: [invalid-exception-caught] +27 | except z as g: + | ^ Object has type `tuple[type[str], ...]` +28 | reveal_type(g) # revealed: Unknown + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid object caught in an exception handler + --> src/mdtest_snippet.py:33:8 + | +31 | {}.get("foo") +32 | # error: [invalid-exception-caught] +33 | except int: + | ^^^ Object has type `` +34 | pass + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno…_(a97274530a7f61c1).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno…_(a97274530a7f61c1).snap new file mode 100644 index 0000000000..3791111451 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno…_(a97274530a7f61c1).snap @@ -0,0 +1,93 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Exception Handling - Special-cased diagnostics for `NotImplemented` +mdtest path: crates/ty_python_semantic/resources/mdtest/exception/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | try: + 2 | # error: [invalid-raise] + 3 | # error: [invalid-raise] + 4 | raise NotImplemented from NotImplemented + 5 | # error: [invalid-exception-caught] + 6 | except NotImplemented: + 7 | pass + 8 | # error: [invalid-exception-caught] + 9 | except (TypeError, NotImplemented): +10 | pass +``` + +# Diagnostics + +``` +error[invalid-raise]: Cannot raise `NotImplemented` + --> src/mdtest_snippet.py:4:11 + | +2 | # error: [invalid-raise] +3 | # error: [invalid-raise] +4 | raise NotImplemented from NotImplemented + | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? +5 | # error: [invalid-exception-caught] +6 | except NotImplemented: + | +info: Can only raise an instance or subclass of `BaseException` +info: rule `invalid-raise` is enabled by default + +``` + +``` +error[invalid-raise]: Cannot use `NotImplemented` as an exception cause + --> src/mdtest_snippet.py:4:31 + | +2 | # error: [invalid-raise] +3 | # error: [invalid-raise] +4 | raise NotImplemented from NotImplemented + | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? +5 | # error: [invalid-exception-caught] +6 | except NotImplemented: + | +info: An exception cause must be an instance of `BaseException`, subclass of `BaseException`, or `None` +info: rule `invalid-raise` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Cannot catch `NotImplemented` in an exception handler + --> src/mdtest_snippet.py:6:8 + | +4 | raise NotImplemented from NotImplemented +5 | # error: [invalid-exception-caught] +6 | except NotImplemented: + | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? +7 | pass +8 | # error: [invalid-exception-caught] + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` + +``` +error[invalid-exception-caught]: Invalid tuple caught in an exception handler + --> src/mdtest_snippet.py:9:8 + | + 7 | pass + 8 | # error: [invalid-exception-caught] + 9 | except (TypeError, NotImplemented): + | ^^^^^^^^^^^^--------------^ + | | + | Invalid element of type `NotImplementedType` + | Did you mean `NotImplementedError`? +10 | pass + | +info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses +info: rule `invalid-exception-caught` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 3e847be1db..7145614f74 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -2380,36 +2380,108 @@ pub(super) fn report_possibly_missing_attribute( }; } +pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( + context: &InferContext<'db, 'ast>, + node: &'ast ast::ExprTuple, + invalid_tuple_nodes: impl IntoIterator)>, +) { + let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { + return; + }; + + let mut diagnostic = builder.into_diagnostic("Invalid tuple caught in an exception handler"); + + for (sub_node, ty) in invalid_tuple_nodes { + let span = context.span(sub_node); + diagnostic.annotate(Annotation::secondary(span.clone()).message(format_args!( + "Invalid element of type `{}`", + ty.display(context.db()) + ))); + if ty.is_notimplemented(context.db()) { + diagnostic.annotate( + Annotation::secondary(span).message("Did you mean `NotImplementedError`?"), + ); + } + } + + diagnostic.info( + "Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses", + ); +} + pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; - builder.into_diagnostic(format_args!( - "Cannot catch object of type `{}` in an exception handler \ - (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", - ty.display(context.db()) - )); + + let mut diagnostic = if ty.is_notimplemented(context.db()) { + let mut diag = + builder.into_diagnostic("Cannot catch `NotImplemented` in an exception handler"); + diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag + } else { + let mut diag = builder.into_diagnostic(format_args!( + "Invalid {thing} caught in an exception handler", + thing = if ty.tuple_instance_spec(context.db()).is_some() { + "tuple" + } else { + "object" + }, + )); + diag.set_primary_message(format_args!( + "Object has type `{}`", + ty.display(context.db()) + )); + diag + }; + + diagnostic.info( + "Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses", + ); } -pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) { - let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { +pub(crate) fn report_invalid_exception_raised( + context: &InferContext, + raised_node: &ast::Expr, + raise_type: Type, +) { + let Some(builder) = context.report_lint(&INVALID_RAISE, raised_node) else { return; }; - builder.into_diagnostic(format_args!( - "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", - ty.display(context.db()) - )); + if raise_type.is_notimplemented(context.db()) { + let mut diagnostic = + builder.into_diagnostic(format_args!("Cannot raise `NotImplemented`",)); + diagnostic.set_primary_message("Did you mean `NotImplementedError`?"); + diagnostic.info("Can only raise an instance or subclass of `BaseException`"); + } else { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot raise object of type `{}`", + raise_type.display(context.db()) + )); + diagnostic.set_primary_message("Not an instance or subclass of `BaseException`"); + } } pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { return; }; - builder.into_diagnostic(format_args!( - "Cannot use object of type `{}` as exception cause \ - (must be a `BaseException` subclass or instance or `None`)", - ty.display(context.db()) - )); + let mut diagnostic = if ty.is_notimplemented(context.db()) { + let mut diag = builder.into_diagnostic(format_args!( + "Cannot use `NotImplemented` as an exception cause", + )); + diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag + } else { + builder.into_diagnostic(format_args!( + "Cannot use object of type `{}` as an exception cause", + ty.display(context.db()) + )) + }; + diagnostic.info( + "An exception cause must be an instance of `BaseException`, \ + subclass of `BaseException`, or `None`", + ); } pub(crate) fn report_instance_layout_conflict( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b95b86db09..0ab7d306b9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -72,9 +72,10 @@ use crate::types::diagnostic::{ report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_generator_function_return_type, - report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, - report_invalid_return_type, report_invalid_type_checking_constant, + report_invalid_exception_raised, report_invalid_exception_tuple_caught, + report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, + report_invalid_or_unsupported_base, report_invalid_return_type, + report_invalid_type_checking_constant, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, @@ -3025,7 +3026,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { let mut builder = UnionBuilder::new(self.db()); - for element in tuple_spec.all_elements().copied() { + let mut invalid_elements = vec![]; + + for (index, element) in tuple_spec.all_elements().enumerate() { builder = builder.add( if element.is_assignable_to(self.db(), type_base_exception) { element.to_instance(self.db()).expect( @@ -3033,13 +3036,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if called on a type assignable to `type[BaseException]`", ) } else { - if let Some(node) = node { - report_invalid_exception_caught(&self.context, node, element); - } + invalid_elements.push((index, element)); Type::unknown() }, ); } + + if !invalid_elements.is_empty() + && let Some(node) = node + { + if let ast::Expr::Tuple(tuple) = node + && !tuple.iter().any(ast::Expr::is_starred_expr) + && Some(tuple.len()) == tuple_spec.len().into_fixed_length() + { + let invalid_elements = invalid_elements + .iter() + .map(|(index, ty)| (&tuple.elts[*index], **ty)); + + report_invalid_exception_tuple_caught(&self.context, tuple, invalid_elements); + } else { + report_invalid_exception_caught(&self.context, node, node_ty); + } + } + builder.build() } else if node_ty.is_assignable_to(self.db(), type_base_exception) { node_ty.to_instance(self.db()).expect(