Skip to content

Commit 8ebecb2

Browse files
authored
[ty] Add subdiagnostic hint if the user wrote X = Any rather than X: Any (#21777)
1 parent 45ac30a commit 8ebecb2

File tree

3 files changed

+185
-0
lines changed

3 files changed

+185
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Diagnostics for invalid attribute access on special forms
2+
3+
<!-- snapshot-diagnostics -->
4+
5+
```py
6+
from typing_extensions import Any, Final, LiteralString, Self
7+
8+
X = Any
9+
10+
class Foo:
11+
X: Final = LiteralString
12+
a: int
13+
b: Self
14+
15+
class Bar:
16+
def __init__(self):
17+
self.y: Final = LiteralString
18+
19+
X.foo # error: [unresolved-attribute]
20+
X.aaaaooooooo # error: [unresolved-attribute]
21+
Foo.X.startswith # error: [unresolved-attribute]
22+
Foo.Bar().y.startswith # error: [unresolved-attribute]
23+
24+
# TODO: false positive (just testing the diagnostic in the meantime)
25+
Foo().b.a # error: [unresolved-attribute]
26+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: special_form_attributes.md - Diagnostics for invalid attribute access on special forms
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import Any, Final, LiteralString, Self
16+
2 |
17+
3 | X = Any
18+
4 |
19+
5 | class Foo:
20+
6 | X: Final = LiteralString
21+
7 | a: int
22+
8 | b: Self
23+
9 |
24+
10 | class Bar:
25+
11 | def __init__(self):
26+
12 | self.y: Final = LiteralString
27+
13 |
28+
14 | X.foo # error: [unresolved-attribute]
29+
15 | X.aaaaooooooo # error: [unresolved-attribute]
30+
16 | Foo.X.startswith # error: [unresolved-attribute]
31+
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
32+
18 |
33+
19 | # TODO: false positive (just testing the diagnostic in the meantime)
34+
20 | Foo().b.a # error: [unresolved-attribute]
35+
```
36+
37+
# Diagnostics
38+
39+
```
40+
error[unresolved-attribute]: Special form `typing.Any` has no attribute `foo`
41+
--> src/mdtest_snippet.py:14:1
42+
|
43+
12 | self.y: Final = LiteralString
44+
13 |
45+
14 | X.foo # error: [unresolved-attribute]
46+
| ^^^^^
47+
15 | X.aaaaooooooo # error: [unresolved-attribute]
48+
16 | Foo.X.startswith # error: [unresolved-attribute]
49+
|
50+
help: Objects with type `Any` have a `foo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any`
51+
help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended
52+
info: rule `unresolved-attribute` is enabled by default
53+
54+
```
55+
56+
```
57+
error[unresolved-attribute]: Special form `typing.Any` has no attribute `aaaaooooooo`
58+
--> src/mdtest_snippet.py:15:1
59+
|
60+
14 | X.foo # error: [unresolved-attribute]
61+
15 | X.aaaaooooooo # error: [unresolved-attribute]
62+
| ^^^^^^^^^^^^^
63+
16 | Foo.X.startswith # error: [unresolved-attribute]
64+
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
65+
|
66+
help: Objects with type `Any` have an `aaaaooooooo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any`
67+
help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended
68+
info: rule `unresolved-attribute` is enabled by default
69+
70+
```
71+
72+
```
73+
error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith`
74+
--> src/mdtest_snippet.py:16:1
75+
|
76+
14 | X.foo # error: [unresolved-attribute]
77+
15 | X.aaaaooooooo # error: [unresolved-attribute]
78+
16 | Foo.X.startswith # error: [unresolved-attribute]
79+
| ^^^^^^^^^^^^^^^^
80+
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
81+
|
82+
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
83+
help: This error may indicate that `Foo.X` was defined as `Foo.X = typing.LiteralString` when `Foo.X: typing.LiteralString` was intended
84+
info: rule `unresolved-attribute` is enabled by default
85+
86+
```
87+
88+
```
89+
error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith`
90+
--> src/mdtest_snippet.py:17:1
91+
|
92+
15 | X.aaaaooooooo # error: [unresolved-attribute]
93+
16 | Foo.X.startswith # error: [unresolved-attribute]
94+
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
95+
| ^^^^^^^^^^^^^^^^^^^^^^
96+
18 |
97+
19 | # TODO: false positive (just testing the diagnostic in the meantime)
98+
|
99+
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
100+
info: rule `unresolved-attribute` is enabled by default
101+
102+
```
103+
104+
```
105+
error[unresolved-attribute]: Special form `typing.Self` has no attribute `a`
106+
--> src/mdtest_snippet.py:20:1
107+
|
108+
19 | # TODO: false positive (just testing the diagnostic in the meantime)
109+
20 | Foo().b.a # error: [unresolved-attribute]
110+
| ^^^^^^^^^
111+
|
112+
info: rule `unresolved-attribute` is enabled by default
113+
114+
```

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use itertools::{Either, EitherOrBoth, Itertools};
44
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
55
use ruff_db::files::File;
66
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
7+
use ruff_db::source::source_text;
78
use ruff_python_ast::visitor::{Visitor, walk_expr};
89
use ruff_python_ast::{
910
self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion,
@@ -9111,6 +9112,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
91119112

91129113
/// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context.
91139114
fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
9115+
fn is_dotted_name(attribute: &ast::Expr) -> bool {
9116+
match attribute {
9117+
ast::Expr::Name(_) => true,
9118+
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value),
9119+
_ => false,
9120+
}
9121+
}
9122+
91149123
let ast::ExprAttribute { value, attr, .. } = attribute;
91159124

91169125
let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default());
@@ -9204,6 +9213,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
92049213
}
92059214
}
92069215

9216+
if let Type::SpecialForm(special_form) = value_type {
9217+
if let Some(builder) =
9218+
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
9219+
{
9220+
let mut diag = builder.into_diagnostic(format_args!(
9221+
"Special form `{special_form}` has no attribute `{attr_name}`",
9222+
));
9223+
if let Ok(defined_type) = value_type.in_type_expression(
9224+
db,
9225+
self.scope(),
9226+
self.typevar_binding_context,
9227+
) && !defined_type.member(db, attr_name).place.is_undefined()
9228+
{
9229+
diag.help(format_args!(
9230+
"Objects with type `{ty}` have a{maybe_n} `{attr_name}` attribute, but the symbol \
9231+
`{special_form}` does not itself inhabit the type `{ty}`",
9232+
maybe_n = if attr_name.starts_with(['a', 'e', 'i', 'o', 'u']) {
9233+
"n"
9234+
} else {
9235+
""
9236+
},
9237+
ty = defined_type.display(self.db())
9238+
));
9239+
if is_dotted_name(value) {
9240+
let source = &source_text(self.db(), self.file())[value.range()];
9241+
diag.help(format_args!(
9242+
"This error may indicate that `{source}` was defined as \
9243+
`{source} = {special_form}` when `{source}: {special_form}` \
9244+
was intended"
9245+
));
9246+
}
9247+
}
9248+
}
9249+
return fallback();
9250+
}
9251+
92079252
let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
92089253
else {
92099254
return fallback();

0 commit comments

Comments
 (0)