From b19ddca69b32b2ee67d16d524f3bb9a594799f5c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 25 Nov 2025 10:29:01 +0000 Subject: [PATCH] [ty] Improve several "Did you mean?" suggestions (#21597) --- .../mdtest/annotations/literal_string.md | 3 +- ..._Module-literal_used_…_(652fec4fd4a6c63a).snap | 10 +- ...es_-_Parameterized_(ec84ce49ea235791).snap | 52 +++++++++ ...ping.TypedDict`_i…_(9df67eb93e3df341).snap | 34 ++++++ .../resources/mdtest/typed_dict.md | 4 +- crates/ty_python_semantic/src/types.rs | 101 ++++++++++-------- .../types/infer/builder/type_expression.rs | 43 ++++++-- 7 files changed, 183 insertions(+), 64 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/literal_string.md_-_`LiteralString`_-_Usages_-_Parameterized_(ec84ce49ea235791).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i…_(9df67eb93e3df341).snap diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 0496dbb4ce..3b0aa2d26c 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -35,6 +35,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form] `LiteralString` cannot be parameterized. + + ```py from typing_extensions import LiteralString @@ -42,7 +44,6 @@ from typing_extensions import LiteralString a: LiteralString[str] # error: [invalid-type-form] -# error: [unresolved-reference] "Name `foo` used when not defined" b: LiteralString["foo"] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap index abe619e045..957a2d13f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap @@ -34,29 +34,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md # Diagnostics ``` -error[invalid-type-form]: Variable of type `` is not allowed in a type expression +error[invalid-type-form]: Module `datetime` is not valid in a type expression --> src/foo.py:3:10 | 1 | import datetime 2 | 3 | def f(x: datetime): ... # error: [invalid-type-form] - | ^^^^^^^^ + | ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`? | -info: Did you mean to use the module's member `datetime.datetime` instead? info: rule `invalid-type-form` is enabled by default ``` ``` -error[invalid-type-form]: Variable of type `` is not allowed in a type expression +error[invalid-type-form]: Module `PIL.Image` is not valid in a type expression --> src/bar.py:3:10 | 1 | from PIL import Image 2 | 3 | def g(x: Image): ... # error: [invalid-type-form] - | ^^^^^ + | ^^^^^ Did you mean to use the module's member `Image.Image`? | -info: Did you mean to use the module's member `Image.Image` instead? info: rule `invalid-type-form` is enabled by default ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/literal_string.md_-_`LiteralString`_-_Usages_-_Parameterized_(ec84ce49ea235791).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/literal_string.md_-_`LiteralString`_-_Usages_-_Parameterized_(ec84ce49ea235791).snap new file mode 100644 index 0000000000..cda41e9fb3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/literal_string.md_-_`LiteralString`_-_Usages_-_Parameterized_(ec84ce49ea235791).snap @@ -0,0 +1,52 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: literal_string.md - `LiteralString` - Usages - Parameterized +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import LiteralString +2 | +3 | # error: [invalid-type-form] +4 | a: LiteralString[str] +5 | +6 | # error: [invalid-type-form] +7 | b: LiteralString["foo"] +``` + +# Diagnostics + +``` +error[invalid-type-form]: `LiteralString` expects no type parameter + --> src/mdtest_snippet.py:4:4 + | +3 | # error: [invalid-type-form] +4 | a: LiteralString[str] + | ^^^^^^^^^^^^^^^^^^ +5 | +6 | # error: [invalid-type-form] + | +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: `LiteralString` expects no type parameter + --> src/mdtest_snippet.py:7:4 + | +6 | # error: [invalid-type-form] +7 | b: LiteralString["foo"] + | -------------^^^^^^^ + | | + | Did you mean `Literal`? + | +info: rule `invalid-type-form` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i…_(9df67eb93e3df341).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i…_(9df67eb93e3df341).snap new file mode 100644 index 0000000000..2cdd83de75 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i…_(9df67eb93e3df341).snap @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: typed_dict.md - `TypedDict` - Error cases - `typing.TypedDict` is not allowed in type expressions +mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypedDict +2 | +3 | # error: [invalid-type-form] "The special form `typing.TypedDict` is not allowed in type expressions" +4 | x: TypedDict = {"name": "Alice"} +``` + +# Diagnostics + +``` +error[invalid-type-form]: The special form `typing.TypedDict` is not allowed in type expressions + --> src/mdtest_snippet.py:4:4 + | +3 | # error: [invalid-type-form] "The special form `typing.TypedDict` is not allowed in type expressions" +4 | x: TypedDict = {"name": "Alice"} + | ^^^^^^^^^ + | +help: You might have meant to use a concrete TypedDict or `collections.abc.Mapping[str, object]` +info: rule `invalid-type-form` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 701dc7848a..06d70dcd02 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -1407,10 +1407,12 @@ msg.content ### `typing.TypedDict` is not allowed in type expressions + + ```py from typing import TypedDict -# error: [invalid-type-form] "The special form `typing.TypedDict` is not allowed in type expressions." +# error: [invalid-type-form] "The special form `typing.TypedDict` is not allowed in type expressions" x: TypedDict = {"name": "Alice"} ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1f5dfc71b8..f90810d5e3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8597,25 +8597,23 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::Field => { f.write_str("`dataclasses.Field` is not allowed in type expressions") } - InvalidTypeExpression::ConstraintSet => { - f.write_str("`ty_extensions.ConstraintSet` is not allowed in type expressions") - } - InvalidTypeExpression::GenericContext => { - f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions") - } - InvalidTypeExpression::Specialization => { - f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions") - } - InvalidTypeExpression::TypedDict => { - f.write_str( - "The special form `typing.TypedDict` is not allowed in type expressions. \ - Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?") - } - InvalidTypeExpression::TypeAlias => { - f.write_str( - "`typing.TypeAlias` is only allowed as the sole annotation on an annotated assignment", - ) - } + InvalidTypeExpression::ConstraintSet => f.write_str( + "`ty_extensions.ConstraintSet` is not allowed in type expressions", + ), + InvalidTypeExpression::GenericContext => f.write_str( + "`ty_extensions.GenericContext` is not allowed in type expressions", + ), + InvalidTypeExpression::Specialization => f.write_str( + "`ty_extensions.GenericContext` is not allowed in type expressions", + ), + InvalidTypeExpression::TypedDict => f.write_str( + "The special form `typing.TypedDict` \ + is not allowed in type expressions", + ), + InvalidTypeExpression::TypeAlias => f.write_str( + "`typing.TypeAlias` is only allowed \ + as the sole annotation on an annotated assignment", + ), InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{qualifier}` is not allowed in type expressions \ @@ -8626,6 +8624,11 @@ 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::ModuleLiteral(module), _) => write!( + f, + "Module `{module}` is not valid in a type expression", + module = module.module(self.db).name(self.db) + ), InvalidTypeExpression::InvalidType(ty, _) => write!( f, "Variable of type `{ty}` is not allowed in a type expression", @@ -8639,35 +8642,39 @@ impl<'db> InvalidTypeExpression<'db> { } fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) { - let InvalidTypeExpression::InvalidType(ty, scope) = self else { - return; - }; - let Type::ModuleLiteral(module_type) = ty else { - return; - }; - let module = module_type.module(db); - let Some(module_name_final_part) = module.name(db).components().next_back() else { - return; - }; - let Some(module_member_with_same_name) = ty - .member(db, module_name_final_part) - .place - .ignore_possibly_undefined() - else { - return; - }; - if module_member_with_same_name - .in_type_expression(db, scope, None) - .is_err() - { - return; - } + if let InvalidTypeExpression::InvalidType(ty, scope) = self { + let Type::ModuleLiteral(module_type) = ty else { + return; + }; + let module = module_type.module(db); + let Some(module_name_final_part) = module.name(db).components().next_back() else { + return; + }; + let Some(module_member_with_same_name) = ty + .member(db, module_name_final_part) + .place + .ignore_possibly_undefined() + else { + return; + }; + if module_member_with_same_name + .in_type_expression(db, scope, None) + .is_err() + { + return; + } - // TODO: showing a diff (and even having an autofix) would be even better - diagnostic.info(format_args!( - "Did you mean to use the module's member \ - `{module_name_final_part}.{module_name_final_part}` instead?" - )); + // TODO: showing a diff (and even having an autofix) would be even better + diagnostic.set_primary_message(format_args!( + "Did you mean to use the module's member \ + `{module_name_final_part}.{module_name_final_part}`?" + )); + } else if let InvalidTypeExpression::TypedDict = self { + diagnostic.help( + "You might have meant to use a concrete TypedDict \ + or `collections.abc.Mapping[str, object]`", + ); + } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 6404364278..0351df5067 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -148,15 +148,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // anything else is an invalid annotation: op => { self.infer_binary_expression(binary, TypeContext::default()); - if let Some(mut diag) = self.report_invalid_type_expression( + self.report_invalid_type_expression( expression, format_args!( "Invalid binary operator `{}` in type annotation", op.as_str() ), - ) { - diag.info("Did you mean to use `|`?"); - } + ); Type::unknown() } } @@ -1446,13 +1444,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } SpecialFormType::LiteralString => { - self.infer_type_expression(arguments_slice); + let arguments = self.infer_expression(arguments_slice, TypeContext::default()); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let mut diag = builder.into_diagnostic(format_args!( - "Type `{special_form}` expected no type parameter", - )); - diag.info("Did you mean to use `Literal[...]` instead?"); + let mut diag = + builder.into_diagnostic("`LiteralString` expects no type parameter"); + + let arguments_as_tuple = arguments.exact_tuple_instance_spec(db); + + let mut argument_elements = arguments_as_tuple + .as_ref() + .map(|tup| Either::Left(tup.all_elements().copied())) + .unwrap_or(Either::Right(std::iter::once(arguments))); + + let probably_meant_literal = argument_elements.all(|ty| match ty { + Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::EnumLiteral(_) + | Type::BooleanLiteral(_) => true, + Type::NominalInstance(instance) => { + instance.has_known_class(db, KnownClass::NoneType) + } + _ => false, + }); + + if probably_meant_literal { + diag.annotate( + self.context + .secondary(&*subscript.value) + .message("Did you mean `Literal`?"), + ); + diag.set_concise_message( + "`LiteralString` expects no type parameter - did you mean `Literal`?", + ); + } } Type::unknown() }