Skip to content

Commit bc9d7f8

Browse files
committed
[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
1 parent 02fd481 commit bc9d7f8

File tree

3 files changed

+141
-7
lines changed

3 files changed

+141
-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,64 @@
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: Module `datetime` has a member with a similar name that is valid in type expressions; perhaps you meant to use that?
46+
info: Try `datetime.datetime`
47+
info: rule `invalid-type-form` is enabled by default
48+
49+
```
50+
51+
```
52+
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
53+
--> src/bar.py:3:10
54+
|
55+
1 | from PIL import Image
56+
2 |
57+
3 | def g(x: Image): ... # error: [invalid-type-form]
58+
| ^^^^^
59+
|
60+
info: Module `Image` has a member with a similar name that is valid in type expressions; perhaps you meant to use that?
61+
info: Try `Image.Image`
62+
info: rule `invalid-type-form` is enabled by default
63+
64+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 45 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
};
@@ -5048,7 +5050,7 @@ impl<'db> Type<'db> {
50485050
)),
50495051
_ => Err(InvalidTypeExpressionError {
50505052
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
5051-
*self
5053+
*self, scope_id
50525054
)],
50535055
fallback_type: Type::unknown(),
50545056
}),
@@ -5762,7 +5764,8 @@ impl<'db> InvalidTypeExpressionError<'db> {
57625764
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else {
57635765
continue;
57645766
};
5765-
builder.into_diagnostic(error.reason(context.db()));
5767+
let diagnostic = builder.into_diagnostic(error.reason(context.db()));
5768+
error.add_subdiagnostics(context.db(), diagnostic);
57665769
}
57675770
}
57685771
fallback_type
@@ -5789,7 +5792,7 @@ enum InvalidTypeExpression<'db> {
57895792
/// and which would require exactly one argument even if they appeared in an annotation expression
57905793
TypeQualifierRequiresOneArgument(KnownInstanceType<'db>),
57915794
/// Some types are always invalid in type expressions
5792-
InvalidType(Type<'db>),
5795+
InvalidType(Type<'db>, ScopeId<'db>),
57935796
}
57945797

57955798
impl<'db> InvalidTypeExpression<'db> {
@@ -5833,7 +5836,7 @@ impl<'db> InvalidTypeExpression<'db> {
58335836
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)",
58345837
q = qualifier.repr(self.db)
58355838
),
5836-
InvalidTypeExpression::InvalidType(ty) => write!(
5839+
InvalidTypeExpression::InvalidType(ty, _) => write!(
58375840
f,
58385841
"Variable of type `{ty}` is not allowed in a type expression",
58395842
ty = ty.display(self.db)
@@ -5844,6 +5847,41 @@ impl<'db> InvalidTypeExpression<'db> {
58445847

58455848
Display { error: self, db }
58465849
}
5850+
5851+
fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) {
5852+
let InvalidTypeExpression::InvalidType(ty, scope) = self else {
5853+
return;
5854+
};
5855+
let Type::ModuleLiteral(module_type) = ty else {
5856+
return;
5857+
};
5858+
let module = module_type.module(db);
5859+
let Some(module_name_final_part) = module.name().components().next_back() else {
5860+
return;
5861+
};
5862+
let Some(module_member_with_same_name) = ty
5863+
.member(db, module_name_final_part)
5864+
.symbol
5865+
.ignore_possibly_unbound()
5866+
else {
5867+
return;
5868+
};
5869+
if module_member_with_same_name
5870+
.in_type_expression(db, scope)
5871+
.is_err()
5872+
{
5873+
return;
5874+
}
5875+
diagnostic.info(format_args!(
5876+
"Module `{module_name_final_part}` has a member with a similar name \
5877+
that is valid in type expressions; perhaps you meant to use that?",
5878+
));
5879+
5880+
// TODO: showing a diff (and even having an autofix) would be even better
5881+
diagnostic.info(format_args!(
5882+
"Try `{module_name_final_part}.{module_name_final_part}`"
5883+
));
5884+
}
58475885
}
58485886

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

0 commit comments

Comments
 (0)