[ty] Improve several "Did you mean?" suggestions (#21597)

This commit is contained in:
Alex Waygood 2025-11-25 10:29:01 +00:00 committed by GitHub
parent 747c39a26a
commit b19ddca69b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 183 additions and 64 deletions

View File

@ -35,6 +35,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
`LiteralString` cannot be parameterized. `LiteralString` cannot be parameterized.
<!-- snapshot-diagnostics -->
```py ```py
from typing_extensions import LiteralString from typing_extensions import LiteralString
@ -42,7 +44,6 @@ from typing_extensions import LiteralString
a: LiteralString[str] a: LiteralString[str]
# error: [invalid-type-form] # error: [invalid-type-form]
# error: [unresolved-reference] "Name `foo` used when not defined"
b: LiteralString["foo"] b: LiteralString["foo"]
``` ```

View File

@ -34,29 +34,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
# Diagnostics # Diagnostics
``` ```
error[invalid-type-form]: Variable of type `<module 'datetime'>` 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 --> src/foo.py:3:10
| |
1 | import datetime 1 | import datetime
2 | 2 |
3 | def f(x: datetime): ... # error: [invalid-type-form] 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 info: rule `invalid-type-form` is enabled by default
``` ```
``` ```
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` 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 --> src/bar.py:3:10
| |
1 | from PIL import Image 1 | from PIL import Image
2 | 2 |
3 | def g(x: Image): ... # error: [invalid-type-form] 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 info: rule `invalid-type-form` is enabled by default
``` ```

View File

@ -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
```

View File

@ -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
```

View File

@ -1407,10 +1407,12 @@ msg.content
### `typing.TypedDict` is not allowed in type expressions ### `typing.TypedDict` is not allowed in type expressions
<!-- snapshot-diagnostics -->
```py ```py
from typing import TypedDict 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"} x: TypedDict = {"name": "Alice"}
``` ```

View File

@ -8597,25 +8597,23 @@ impl<'db> InvalidTypeExpression<'db> {
InvalidTypeExpression::Field => { InvalidTypeExpression::Field => {
f.write_str("`dataclasses.Field` is not allowed in type expressions") f.write_str("`dataclasses.Field` is not allowed in type expressions")
} }
InvalidTypeExpression::ConstraintSet => { InvalidTypeExpression::ConstraintSet => f.write_str(
f.write_str("`ty_extensions.ConstraintSet` is not allowed in type expressions") "`ty_extensions.ConstraintSet` is not allowed in type expressions",
} ),
InvalidTypeExpression::GenericContext => { InvalidTypeExpression::GenericContext => f.write_str(
f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions") "`ty_extensions.GenericContext` is not allowed in type expressions",
} ),
InvalidTypeExpression::Specialization => { InvalidTypeExpression::Specialization => f.write_str(
f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions") "`ty_extensions.GenericContext` is not allowed in type expressions",
} ),
InvalidTypeExpression::TypedDict => { InvalidTypeExpression::TypedDict => f.write_str(
f.write_str( "The special form `typing.TypedDict` \
"The special form `typing.TypedDict` is not allowed in type expressions. \ 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(
InvalidTypeExpression::TypeAlias => { "`typing.TypeAlias` is only allowed \
f.write_str( as the sole annotation on an annotated assignment",
"`typing.TypeAlias` is only allowed as the sole annotation on an annotated assignment", ),
)
}
InvalidTypeExpression::TypeQualifier(qualifier) => write!( InvalidTypeExpression::TypeQualifier(qualifier) => write!(
f, f,
"Type qualifier `{qualifier}` is not allowed in type expressions \ "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 \ "Type qualifier `{qualifier}` is not allowed in type expressions \
(only in annotation expressions, and only with exactly one argument)", (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!( InvalidTypeExpression::InvalidType(ty, _) => write!(
f, f,
"Variable of type `{ty}` is not allowed in a type expression", "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) { fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) {
let InvalidTypeExpression::InvalidType(ty, scope) = self else { if let InvalidTypeExpression::InvalidType(ty, scope) = self {
return; let Type::ModuleLiteral(module_type) = ty 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 {
let module = module_type.module(db); return;
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)
let Some(module_member_with_same_name) = ty .place
.member(db, module_name_final_part) .ignore_possibly_undefined()
.place else {
.ignore_possibly_undefined() return;
else { };
return; if module_member_with_same_name
}; .in_type_expression(db, scope, None)
if module_member_with_same_name .is_err()
.in_type_expression(db, scope, None) {
.is_err() return;
{ }
return;
}
// TODO: showing a diff (and even having an autofix) would be even better // TODO: showing a diff (and even having an autofix) would be even better
diagnostic.info(format_args!( diagnostic.set_primary_message(format_args!(
"Did you mean to use the module's member \ "Did you mean to use the module's member \
`{module_name_final_part}.{module_name_final_part}` instead?" `{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]`",
);
}
} }
} }

View File

@ -148,15 +148,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
// anything else is an invalid annotation: // anything else is an invalid annotation:
op => { op => {
self.infer_binary_expression(binary, TypeContext::default()); self.infer_binary_expression(binary, TypeContext::default());
if let Some(mut diag) = self.report_invalid_type_expression( self.report_invalid_type_expression(
expression, expression,
format_args!( format_args!(
"Invalid binary operator `{}` in type annotation", "Invalid binary operator `{}` in type annotation",
op.as_str() op.as_str()
), ),
) { );
diag.info("Did you mean to use `|`?");
}
Type::unknown() Type::unknown()
} }
} }
@ -1446,13 +1444,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
Type::unknown() Type::unknown()
} }
SpecialFormType::LiteralString => { 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) { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
let mut diag = builder.into_diagnostic(format_args!( let mut diag =
"Type `{special_form}` expected no type parameter", builder.into_diagnostic("`LiteralString` expects no type parameter");
));
diag.info("Did you mean to use `Literal[...]` instead?"); 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() Type::unknown()
} }