From 5913997c7228a81872b093bad6dc0a95dae66e12 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 13 May 2025 09:00:20 -0400 Subject: [PATCH] [ty] Improve diagnostics for `assert_type` and `assert_never` (#18050) --- .../mdtest/directives/assert_never.md | 10 +- .../mdtest/directives/assert_type.md | 2 + ..._`assert_never`_-_Basic_functionality.snap | 189 ++++++++++++++++++ ...ssert_type.md_-_`assert_type`_-_Basic.snap | 38 ++++ .../src/types/diagnostic.rs | 11 +- crates/ty_python_semantic/src/types/infer.rs | 49 ++++- 6 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality.snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic.snap diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 1866515a7b..958f2c04f9 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -2,6 +2,8 @@ ## Basic functionality + + `assert_never` makes sure that the type of the argument is `Never`. If it is not, a `type-assertion-failure` diagnostic is emitted. @@ -58,7 +60,7 @@ def if_else_isinstance_error(obj: A | B): elif isinstance(obj, C): pass else: - # error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def if_else_singletons_success(obj: Literal[1, "a"] | None): @@ -79,7 +81,7 @@ def if_else_singletons_error(obj: Literal[1, "a"] | None): elif obj is None: pass else: - # error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def match_singletons_success(obj: Literal[1, "a"] | None): @@ -92,7 +94,7 @@ def match_singletons_success(obj: Literal[1, "a"] | None): pass case _ as obj: # TODO: Ideally, we would not emit an error here - # error: [type-assertion-failure] "Expected type `Never`, got `@Todo" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def match_singletons_error(obj: Literal[1, "a"] | None): @@ -106,6 +108,6 @@ def match_singletons_error(obj: Literal[1, "a"] | None): case _ as obj: # TODO: We should emit an error here, but the message should # show the type `Literal["a"]` instead of `@Todo(…)`. - # error: [type-assertion-failure] "Expected type `Never`, got `@Todo" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index 336e82e0ee..07ad5d555b 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -2,6 +2,8 @@ ## Basic + + ```py from typing_extensions import assert_type diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality.snap new file mode 100644 index 0000000000..6b4c6d3625 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality.snap @@ -0,0 +1,189 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: assert_never.md - `assert_never` - Basic functionality +mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import assert_never, Never, Any + 2 | from ty_extensions import Unknown + 3 | + 4 | def _(never: Never, any_: Any, unknown: Unknown, flag: bool): + 5 | assert_never(never) # fine + 6 | + 7 | assert_never(0) # error: [type-assertion-failure] + 8 | assert_never("") # error: [type-assertion-failure] + 9 | assert_never(None) # error: [type-assertion-failure] +10 | assert_never([]) # error: [type-assertion-failure] +11 | assert_never({}) # error: [type-assertion-failure] +12 | assert_never(()) # error: [type-assertion-failure] +13 | assert_never(1 if flag else never) # error: [type-assertion-failure] +14 | +15 | assert_never(any_) # error: [type-assertion-failure] +16 | assert_never(unknown) # error: [type-assertion-failure] +``` + +# Diagnostics + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:7:5 + | +5 | assert_never(never) # fine +6 | +7 | assert_never(0) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^-^ + | | + | Inferred type of argument is `Literal[0]` +8 | assert_never("") # error: [type-assertion-failure] +9 | assert_never(None) # error: [type-assertion-failure] + | +info: `Never` and `Literal[0]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:8:5 + | + 7 | assert_never(0) # error: [type-assertion-failure] + 8 | assert_never("") # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `Literal[""]` + 9 | assert_never(None) # error: [type-assertion-failure] +10 | assert_never([]) # error: [type-assertion-failure] + | +info: `Never` and `Literal[""]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:9:5 + | + 7 | assert_never(0) # error: [type-assertion-failure] + 8 | assert_never("") # error: [type-assertion-failure] + 9 | assert_never(None) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^----^ + | | + | Inferred type of argument is `None` +10 | assert_never([]) # error: [type-assertion-failure] +11 | assert_never({}) # error: [type-assertion-failure] + | +info: `Never` and `None` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:10:5 + | + 8 | assert_never("") # error: [type-assertion-failure] + 9 | assert_never(None) # error: [type-assertion-failure] +10 | assert_never([]) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `list[Unknown]` +11 | assert_never({}) # error: [type-assertion-failure] +12 | assert_never(()) # error: [type-assertion-failure] + | +info: `Never` and `list[Unknown]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:11:5 + | + 9 | assert_never(None) # error: [type-assertion-failure] +10 | assert_never([]) # error: [type-assertion-failure] +11 | assert_never({}) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `dict[Unknown, Unknown]` +12 | assert_never(()) # error: [type-assertion-failure] +13 | assert_never(1 if flag else never) # error: [type-assertion-failure] + | +info: `Never` and `dict[Unknown, Unknown]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:12:5 + | +10 | assert_never([]) # error: [type-assertion-failure] +11 | assert_never({}) # error: [type-assertion-failure] +12 | assert_never(()) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `tuple[()]` +13 | assert_never(1 if flag else never) # error: [type-assertion-failure] + | +info: `Never` and `tuple[()]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:13:5 + | +11 | assert_never({}) # error: [type-assertion-failure] +12 | assert_never(()) # error: [type-assertion-failure] +13 | assert_never(1 if flag else never) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--------------------^ + | | + | Inferred type of argument is `Literal[1]` +14 | +15 | assert_never(any_) # error: [type-assertion-failure] + | +info: `Never` and `Literal[1]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:15:5 + | +13 | assert_never(1 if flag else never) # error: [type-assertion-failure] +14 | +15 | assert_never(any_) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^----^ + | | + | Inferred type of argument is `Any` +16 | assert_never(unknown) # error: [type-assertion-failure] + | +info: `Never` and `Any` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:16:5 + | +15 | assert_never(any_) # error: [type-assertion-failure] +16 | assert_never(unknown) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^-------^ + | | + | Inferred type of argument is `Unknown` + | +info: `Never` and `Unknown` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic.snap new file mode 100644 index 0000000000..4b9da52c7b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic.snap @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: assert_type.md - `assert_type` - Basic +mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import assert_type +2 | +3 | def _(x: int): +4 | assert_type(x, int) # fine +5 | assert_type(x, str) # error: [type-assertion-failure] +``` + +# Diagnostics + +``` +error[type-assertion-failure]: Argument does not have asserted type `str` + --> src/mdtest_snippet.py:5:5 + | +3 | def _(x: int): +4 | assert_type(x, int) # fine +5 | assert_type(x, str) # error: [type-assertion-failure] + | ^^^^^^^^^^^^-^^^^^^ + | | + | Inferred type of argument is `int` + | +info: `str` and `int` are not equivalent types +info: rule `type-assertion-failure` 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 69549051f0..041abbd196 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -11,7 +11,7 @@ use crate::types::string_annotation::{ RAW_STRING_TYPE_ANNOTATION, }; use crate::types::{protocol_class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; -use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; @@ -1543,7 +1543,7 @@ pub(super) fn report_invalid_return_type( return; }; - let return_type_span = Span::from(context.file()).with_range(return_type_range.range()); + let return_type_span = context.span(return_type_range); let mut diag = builder.into_diagnostic("Return type does not match returned value"); diag.set_primary_message(format_args!( @@ -1849,16 +1849,13 @@ pub(crate) fn report_duplicate_bases( ), ); sub_diagnostic.annotate( - Annotation::secondary( - Span::from(context.file()).with_range(bases_list[*first_index].range()), - ) - .message(format_args!( + Annotation::secondary(context.span(&bases_list[*first_index])).message(format_args!( "Class `{duplicate_name}` first included in bases list here" )), ); for index in later_indices { sub_diagnostic.annotate( - Annotation::primary(Span::from(context.file()).with_range(bases_list[*index].range())) + Annotation::primary(context.span(&bases_list[*index])) .message(format_args!("Class `{duplicate_name}` later repeated here")), ); } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 8ac4ec9ba0..7d85d5dd52 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4834,12 +4834,27 @@ impl<'db> TypeInferenceBuilder<'db> { &TYPE_ASSERTION_FAILURE, call_expression, ) { - builder.into_diagnostic(format_args!( - "Actual type `{}` is not the same \ - as asserted type `{}`", - actual_ty.display(self.db()), - asserted_ty.display(self.db()), - )); + let mut diagnostic = + builder.into_diagnostic(format_args!( + "Argument does not have asserted type `{}`", + asserted_ty.display(self.db()), + )); + diagnostic.annotate( + Annotation::secondary(self.context.span( + &call_expression.arguments.args[0], + )) + .message(format_args!( + "Inferred type of argument is `{}`", + actual_ty.display(self.db()), + )), + ); + diagnostic.info( + format_args!( + "`{asserted_type}` and `{inferred_type}` are not equivalent types", + asserted_type = asserted_ty.display(self.db()), + inferred_type = actual_ty.display(self.db()), + ) + ); } } } @@ -4851,10 +4866,24 @@ impl<'db> TypeInferenceBuilder<'db> { &TYPE_ASSERTION_FAILURE, call_expression, ) { - builder.into_diagnostic(format_args!( - "Expected type `Never`, got `{}` instead", - actual_ty.display(self.db()), - )); + let mut diagnostic = builder.into_diagnostic( + "Argument does not have asserted type `Never`", + ); + diagnostic.annotate( + Annotation::secondary(self.context.span( + &call_expression.arguments.args[0], + )) + .message(format_args!( + "Inferred type of argument is `{}`", + actual_ty.display(self.db()) + )), + ); + diagnostic.info( + format_args!( + "`Never` and `{inferred_type}` are not equivalent types", + inferred_type = actual_ty.display(self.db()), + ) + ); } } }