[ty] Offer "Did you mean...?" suggestions for unresolved from imports and unresolved attributes (#18705)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Alex Waygood 2025-06-17 11:10:34 +01:00 committed by GitHub
parent c7e020df6b
commit 913f136d33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 794 additions and 96 deletions

View file

@ -2167,6 +2167,57 @@ reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
```
## Suggestions for obvious typos
<!-- snapshot-diagnostics -->
For obvious typos, we add a "Did you mean...?" suggestion to the diagnostic.
```py
import collections
print(collections.dequee) # error: [unresolved-attribute]
```
But the suggestion is suppressed if the only close matches start with a leading underscore:
```py
class Foo:
_bar = 42
print(Foo.bar) # error: [unresolved-attribute]
```
The suggestion is not suppressed if the typo itself starts with a leading underscore, however:
```py
print(Foo._barr) # error: [unresolved-attribute]
```
And in method contexts, the suggestion is never suppressed if accessing an attribute on an instance
of the method's enclosing class:
```py
class Bar:
_attribute = 42
def f(self, x: "Bar"):
# TODO: we should emit `[unresolved-attribute]` here, should have the same behaviour as `x.attribute` below
print(self.attribute)
# We give a suggestion here, even though the only good candidates start with underscores and the typo does not,
# because we're in a method context and `x` is an instance of the enclosing class.
print(x.attribute) # error: [unresolved-attribute]
class Baz:
def f(self, x: Bar):
# No suggestion is given here, because:
# - the good suggestions all start with underscores
# - the typo does not start with an underscore
# - We *are* in a method context, but `x` is not an instance of the enclosing class
print(x.attribute) # error: [unresolved-attribute]
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View file

@ -205,3 +205,39 @@ python-version = "3.13"
import aifc # error: [unresolved-import]
from distutils import sysconfig # error: [unresolved-import]
```
## `from` import that has a typo
We offer a "Did you mean?" subdiagnostic suggestion if there's a name in the module that's
reasonably similar to the unresolved member.
<!-- snapshot-diagnostics -->
`foo.py`:
```py
from collections import dequee # error: [unresolved-import]
```
However, we suppress the suggestion if the only close matches in the module start with a leading
underscore:
`bar.py`:
```py
from baz import foo # error: [unresolved-import]
```
`baz.py`:
```py
_foo = 42
```
The suggestion is never suppressed if the typo itself starts with a leading underscore, however:
`eggs.py`:
```py
from baz import _fooo # error: [unresolved-import]
```

View file

@ -0,0 +1,115 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attributes.md - Attributes - Suggestions for obvious typos
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
---
# Python source files
## mdtest_snippet.py
```
1 | import collections
2 |
3 | print(collections.dequee) # error: [unresolved-attribute]
4 | class Foo:
5 | _bar = 42
6 |
7 | print(Foo.bar) # error: [unresolved-attribute]
8 | print(Foo._barr) # error: [unresolved-attribute]
9 | class Bar:
10 | _attribute = 42
11 |
12 | def f(self, x: "Bar"):
13 | # TODO: we should emit `[unresolved-attribute]` here, should have the same behaviour as `x.attribute` below
14 | print(self.attribute)
15 |
16 | # We give a suggestion here, even though the only good candidates start with underscores and the typo does not,
17 | # because we're in a method context and `x` is an instance of the enclosing class.
18 | print(x.attribute) # error: [unresolved-attribute]
19 |
20 | class Baz:
21 | def f(self, x: Bar):
22 | # No suggestion is given here, because:
23 | # - the good suggestions all start with underscores
24 | # - the typo does not start with an underscore
25 | # - We *are* in a method context, but `x` is not an instance of the enclosing class
26 | print(x.attribute) # error: [unresolved-attribute]
```
# Diagnostics
```
error[unresolved-attribute]: Type `<module 'collections'>` has no attribute `dequee`
--> src/mdtest_snippet.py:3:7
|
1 | import collections
2 |
3 | print(collections.dequee) # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^ Did you mean `deque`?
4 | class Foo:
5 | _bar = 42
|
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<class 'Foo'>` has no attribute `bar`
--> src/mdtest_snippet.py:7:7
|
5 | _bar = 42
6 |
7 | print(Foo.bar) # error: [unresolved-attribute]
| ^^^^^^^
8 | print(Foo._barr) # error: [unresolved-attribute]
9 | class Bar:
|
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<class 'Foo'>` has no attribute `_barr`
--> src/mdtest_snippet.py:8:7
|
7 | print(Foo.bar) # error: [unresolved-attribute]
8 | print(Foo._barr) # error: [unresolved-attribute]
| ^^^^^^^^^ Did you mean `_bar`?
9 | class Bar:
10 | _attribute = 42
|
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `Bar` has no attribute `attribute`
--> src/mdtest_snippet.py:18:15
|
16 | # We give a suggestion here, even though the only good candidates start with underscores and the typo does not,
17 | # because we're in a method context and `x` is an instance of the enclosing class.
18 | print(x.attribute) # error: [unresolved-attribute]
| ^^^^^^^^^^^ Did you mean `_attribute`?
19 |
20 | class Baz:
|
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `Bar` has no attribute `attribute`
--> src/mdtest_snippet.py:26:15
|
24 | # - the typo does not start with an underscore
25 | # - We *are* in a method context, but `x` is not an instance of the enclosing class
26 | print(x.attribute) # error: [unresolved-attribute]
| ^^^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
```

View file

@ -0,0 +1,69 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - `from` import that has a typo
mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## foo.py
```
1 | from collections import dequee # error: [unresolved-import]
```
## bar.py
```
1 | from baz import foo # error: [unresolved-import]
```
## baz.py
```
1 | _foo = 42
```
## eggs.py
```
1 | from baz import _fooo # error: [unresolved-import]
```
# Diagnostics
```
error[unresolved-import]: Module `collections` has no member `dequee`
--> src/foo.py:1:25
|
1 | from collections import dequee # error: [unresolved-import]
| ^^^^^^ Did you mean `deque`?
|
info: rule `unresolved-import` is enabled by default
```
```
error[unresolved-import]: Module `baz` has no member `foo`
--> src/bar.py:1:17
|
1 | from baz import foo # error: [unresolved-import]
| ^^^
|
info: rule `unresolved-import` is enabled by default
```
```
error[unresolved-import]: Module `baz` has no member `_fooo`
--> src/eggs.py:1:17
|
1 | from baz import _fooo # error: [unresolved-import]
| ^^^^^ Did you mean `_foo`?
|
info: rule `unresolved-import` is enabled by default
```