Skip to content

Commit 02394b8

Browse files
authored
[ty] Improve invalid-type-form diagnostic where a module-literal type is used in a type expression and the module has a member which would be valid in a type expression (#18244)
1 parent 4146339 commit 02394b8

File tree

3 files changed

+136
-7
lines changed

3 files changed

+136
-7
lines changed

crates/ty_python_semantic/resources/mdtest/annotations/invalid.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,35 @@ def _(
113113
reveal_type(f) # revealed: Unknown
114114
reveal_type(g) # revealed: Unknown
115115
```
116+
117+
## Diagnostics for common errors
118+
119+
<!-- snapshot-diagnostics -->
120+
121+
### Module-literal used when you meant to use a class from that module
122+
123+
It's pretty common in Python to accidentally use a module-literal type in a type expression when you
124+
*meant* to use a class by the same name that comes from that module. We emit a nice subdiagnostic
125+
for this case:
126+
127+
`foo.py`:
128+
129+
```py
130+
import datetime
131+
132+
def f(x: datetime): ... # error: [invalid-type-form]
133+
```
134+
135+
`PIL/Image.py`:
136+
137+
```py
138+
class Image: ...
139+
```
140+
141+
`bar.py`:
142+
143+
```py
144+
from PIL import Image
145+
146+
def g(x: Image): ... # error: [invalid-type-form]
147+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Module-literal used when you meant to use a class from that module
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
8+
---
9+
10+
# Python source files
11+
12+
## foo.py
13+
14+
```
15+
1 | import datetime
16+
2 |
17+
3 | def f(x: datetime): ... # error: [invalid-type-form]
18+
```
19+
20+
## PIL/Image.py
21+
22+
```
23+
1 | class Image: ...
24+
```
25+
26+
## bar.py
27+
28+
```
29+
1 | from PIL import Image
30+
2 |
31+
3 | def g(x: Image): ... # error: [invalid-type-form]
32+
```
33+
34+
# Diagnostics
35+
36+
```
37+
error[invalid-type-form]: Variable of type `<module 'datetime'>` is not allowed in a type expression
38+
--> src/foo.py:3:10
39+
|
40+
1 | import datetime
41+
2 |
42+
3 | def f(x: datetime): ... # error: [invalid-type-form]
43+
| ^^^^^^^^
44+
|
45+
info: Did you mean to use the module's member `datetime.datetime` instead?
46+
info: rule `invalid-type-form` is enabled by default
47+
48+
```
49+
50+
```
51+
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
52+
--> src/bar.py:3:10
53+
|
54+
1 | from PIL import Image
55+
2 |
56+
3 | def g(x: Image): ... # error: [invalid-type-form]
57+
| ^^^^^
58+
|
59+
info: Did you mean to use the module's member `Image.Image` instead?
60+
info: rule `invalid-type-form` is enabled by default
61+
62+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4821,7 +4821,7 @@ impl<'db> Type<'db> {
48214821
pub fn in_type_expression(
48224822
&self,
48234823
db: &'db dyn Db,
4824-
scope_id: ScopeId,
4824+
scope_id: ScopeId<'db>,
48254825
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
48264826
match self {
48274827
// Special cases for `float` and `complex`
@@ -4872,7 +4872,9 @@ impl<'db> Type<'db> {
48724872
| Type::BoundSuper(_)
48734873
| Type::ProtocolInstance(_)
48744874
| Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
4875-
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)],
4875+
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
4876+
*self, scope_id
4877+
)],
48764878
fallback_type: Type::unknown(),
48774879
}),
48784880

@@ -4910,7 +4912,7 @@ impl<'db> Type<'db> {
49104912
return Err(InvalidTypeExpressionError {
49114913
fallback_type: Type::unknown(),
49124914
invalid_expressions: smallvec::smallvec![
4913-
InvalidTypeExpression::InvalidType(*self)
4915+
InvalidTypeExpression::InvalidType(*self, scope_id)
49144916
],
49154917
});
49164918
};
@@ -5037,7 +5039,7 @@ impl<'db> Type<'db> {
50375039
)),
50385040
_ => Err(InvalidTypeExpressionError {
50395041
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
5040-
*self
5042+
*self, scope_id
50415043
)],
50425044
fallback_type: Type::unknown(),
50435045
}),
@@ -5751,7 +5753,8 @@ impl<'db> InvalidTypeExpressionError<'db> {
57515753
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else {
57525754
continue;
57535755
};
5754-
builder.into_diagnostic(error.reason(context.db()));
5756+
let diagnostic = builder.into_diagnostic(error.reason(context.db()));
5757+
error.add_subdiagnostics(context.db(), diagnostic);
57555758
}
57565759
}
57575760
fallback_type
@@ -5778,7 +5781,7 @@ enum InvalidTypeExpression<'db> {
57785781
/// and which would require exactly one argument even if they appeared in an annotation expression
57795782
TypeQualifierRequiresOneArgument(KnownInstanceType<'db>),
57805783
/// Some types are always invalid in type expressions
5781-
InvalidType(Type<'db>),
5784+
InvalidType(Type<'db>, ScopeId<'db>),
57825785
}
57835786

57845787
impl<'db> InvalidTypeExpression<'db> {
@@ -5822,7 +5825,7 @@ impl<'db> InvalidTypeExpression<'db> {
58225825
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)",
58235826
q = qualifier.repr(self.db)
58245827
),
5825-
InvalidTypeExpression::InvalidType(ty) => write!(
5828+
InvalidTypeExpression::InvalidType(ty, _) => write!(
58265829
f,
58275830
"Variable of type `{ty}` is not allowed in a type expression",
58285831
ty = ty.display(self.db)
@@ -5833,6 +5836,38 @@ impl<'db> InvalidTypeExpression<'db> {
58335836

58345837
Display { error: self, db }
58355838
}
5839+
5840+
fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) {
5841+
let InvalidTypeExpression::InvalidType(ty, scope) = self else {
5842+
return;
5843+
};
5844+
let Type::ModuleLiteral(module_type) = ty else {
5845+
return;
5846+
};
5847+
let module = module_type.module(db);
5848+
let Some(module_name_final_part) = module.name().components().next_back() else {
5849+
return;
5850+
};
5851+
let Some(module_member_with_same_name) = ty
5852+
.member(db, module_name_final_part)
5853+
.symbol
5854+
.ignore_possibly_unbound()
5855+
else {
5856+
return;
5857+
};
5858+
if module_member_with_same_name
5859+
.in_type_expression(db, scope)
5860+
.is_err()
5861+
{
5862+
return;
5863+
}
5864+
5865+
// TODO: showing a diff (and even having an autofix) would be even better
5866+
diagnostic.info(format_args!(
5867+
"Did you mean to use the module's member \
5868+
`{module_name_final_part}.{module_name_final_part}` instead?"
5869+
));
5870+
}
58365871
}
58375872

58385873
/// Whether this typecar was created via the legacy `TypeVar` constructor, or using PEP 695 syntax.

0 commit comments

Comments
 (0)