[red-knot] Add more tests for `*` imports (#16955)

## Summary

This PR separates out the entirely new tests from
https://github.com/astral-sh/ruff/pull/16923 into a standalone PR. I'll
rebase https://github.com/astral-sh/ruff/pull/16923 on top of this
branch.

The reasons for separating it out are:
- It should make it clearer to see in
<https://github.com/astral-sh/ruff/pull/16923> exactly how the
functionality is changing (we can see the assertions in the tests
_change_, which isn't so obvious if the tests are entirely new)
- The diff on <https://github.com/astral-sh/ruff/pull/16923> is getting
pretty big; this should reduce the diff on that PR somewhat
- These tests seem useful in and of themselves, so even if we need to do
a wholesale revert of <https://github.com/astral-sh/ruff/pull/16923> for
whatever reason, it'll be nice to keep the tests

## Test Plan

`cargo test -p red_knot_python_semantic`
This commit is contained in:
Alex Waygood 2025-03-24 12:39:16 -04:00 committed by GitHub
parent 85b7f808e1
commit 66d0cf2a72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 279 additions and 0 deletions

View File

@ -148,6 +148,201 @@ reveal_type(X) # revealed: Unknown
reveal_type(Y) # revealed: Unknown reveal_type(Y) # revealed: Unknown
``` ```
### Global-scope symbols defined in many other ways
`a.py`:
```py
import typing
from collections import OrderedDict
from collections import OrderedDict as Foo
A, B = 1, (C := 2)
D: (E := 4) = (F := 5) # error: [invalid-type-form]
for G in [1]:
...
for (H := 4).whatever in [2]: # error: [unresolved-attribute]
...
class I: ...
def J(): ...
type K = int
with () as L: # error: [invalid-context-manager]
...
match 42:
case {"something": M}:
...
case [*N]:
...
case [O]:
...
case P | Q:
...
case object(foo=R):
...
case object(S):
...
case T:
...
```
`b.py`:
```py
from a import *
# fmt: off
print((
# TODO: false positive
A, # error: [unresolved-reference]
# TODO: false positive
B, # error: [unresolved-reference]
# TODO: false positive
C, # error: [unresolved-reference]
# TODO: false positive
D, # error: [unresolved-reference]
# TODO: false positive
E, # error: [unresolved-reference]
# TODO: false positive
F, # error: [unresolved-reference]
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
G, # error: [unresolved-reference] "Name `G` used when not defined"
# TODO: false positive
H, # error: [unresolved-reference]
# TODO: false positive
I, # error: [unresolved-reference]
# TODO: false positive
J, # error: [unresolved-reference]
# TODO: false positive
K, # error: [unresolved-reference]
# TODO: false positive
L, # error: [unresolved-reference]
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
M, # error: [unresolved-reference] "Name `M` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
N, # error: [unresolved-reference] "Name `N` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
O, # error: [unresolved-reference] "Name `O` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
P, # error: [unresolved-reference] "Name `P` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
Q, # error: [unresolved-reference] "Name `Q` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
R, # error: [unresolved-reference] "Name `R` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
S, # error: [unresolved-reference] "Name `S` used when not defined"
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
T, # error: [unresolved-reference] "Name `T` used when not defined"
# TODO: false positive
typing, # error: [unresolved-reference]
# TODO: false positive
OrderedDict, # error: [unresolved-reference]
# TODO: false positive
Foo, # error: [unresolved-reference]
))
```
### Definitions in function-like scopes are not global definitions
Except for some cases involving walrus expressions inside comprehension scopes.
`a.py`:
```py
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
[a for a in Iterable()]
{b for b in Iterable()}
{c: c for c in Iterable()}
(d for d in Iterable())
lambda e: (f := 42)
# Definitions created by walruses in a comprehension scope are unique;
# they "leak out" of the scope and are stored in the surrounding scope
[(g := h * 2) for h in Iterable()]
[i for j in Iterable() if (i := j - 10) > 0]
{(k := l * 2): (m := l * 3) for l in Iterable()}
```
`b.py`:
```py
from a import *
# error: [unresolved-reference]
reveal_type(a) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(b) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(c) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(d) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(e) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(f) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(h) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(j) # revealed: Unknown
# TODO: these should all reveal `Unknown | int` and should not have diagnostics.
# (We don't generally model elsewhere in red-knot that bindings from walruses
# "leak" from comprehension scopes into outer scopes, but we should.)
# See https://github.com/astral-sh/ruff/issues/16954
#
# error: [unresolved-reference]
reveal_type(g) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(i) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(k) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(m) # revealed: Unknown
```
### An annotation without a value is a definition in a stub but not a `.py` file
`a.pyi`:
```pyi
X: bool
```
`b.py`:
```py
Y: bool
```
`c.py`:
```py
from a import *
from b import *
# TODO: this is a false positive, should reveal `bool`
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown
# but this diagnostic is accurate!
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
```
### Global-scope names starting with underscores ### Global-scope names starting with underscores
Global-scope names starting with underscores are not imported from a `*` import (unless the module Global-scope names starting with underscores are not imported from a `*` import (unless the module
@ -263,11 +458,14 @@ if sys.version_info >= (3, 11):
X: bool = True X: bool = True
else: else:
Y: bool = False Y: bool = False
Z: int = 42
``` ```
`b.py`: `b.py`:
```py ```py
Z: bool = True
from a import * from a import *
# TODO should not error, should reveal `bool` # TODO should not error, should reveal `bool`
@ -276,6 +474,12 @@ reveal_type(X) # revealed: Unknown
# error: [unresolved-reference] # error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown reveal_type(Y) # revealed: Unknown
# The `*` import should not be considered a redefinition
# of the global variable in this module, as the symbol in
# the `a` module is in a branch that is statically known
# to be dead code given the `python-version` configuration.
reveal_type(Z) # revealed: Literal[True]
``` ```
### Relative `*` imports ### Relative `*` imports
@ -662,6 +866,40 @@ reveal_type(X) # revealed: Unknown
reveal_type(Y) # revealed: Unknown reveal_type(Y) # revealed: Unknown
``` ```
## `global` statements in non-global scopes
A `global` statement in a nested function scope, combined with a definition in the same function
scope of the name that was declared `global`, can add a symbol to the global namespace.
`a.py`:
```py
def f():
global g, h
g: bool = True
f()
```
`b.py`:
```py
from a import *
# TODO: false positive, should be `Literal[f]` with no diagnostic
# error: [unresolved-reference]
reveal_type(f) # revealed: Unknown
# TODO: false positive, should be `bool` with no diagnostic
# error: [unresolved-reference]
reveal_type(g) # revealed: Unknown
# this diagnostic is accurate, though!
# error: [unresolved-reference]
reveal_type(h) # revealed: Unknown
```
## Integration test: `collections.abc` ## Integration test: `collections.abc`
The `collections.abc` standard-library module provides a good integration test, as all its symbols The `collections.abc` standard-library module provides a good integration test, as all its symbols
@ -711,5 +949,46 @@ def f():
reveal_type(X) # revealed: Unknown reveal_type(X) # revealed: Unknown
``` ```
### `*` combined with other aliases in the list
`a.py`:
```py
X: bool = True
_Y: bool = False
_Z: bool = True
```
`b.py`:
<!-- blacken-docs:off -->
```py
from a import *, _Y # error: [invalid-syntax]
# The import statement above is invalid syntax,
# but it's pretty obvious that the user wanted to do a `*` import,
# so we should import all public names from `a` anyway, to minimize cascading errors
#
# TODO: get rid of this error, reveal `bool`
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown
reveal_type(_Y) # revealed: bool
```
These tests are more to assert that we don't panic on these various kinds of invalid syntax than
anything else:
`c.py`:
```py
from a import *, _Y # error: [invalid-syntax]
from a import _Y, *, _Z # error: [invalid-syntax]
from a import *, _Y as fooo # error: [invalid-syntax]
from a import *, *, _Y # error: [invalid-syntax]
```
<!-- blacken-docs:on -->
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement [python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols [typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols