Compare commits
5 Commits
0.7.1
...
micha/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ace116342c | ||
|
|
6aaf1d9446 | ||
|
|
5eb87aa56e | ||
|
|
085a43a262 | ||
|
|
32b57b2ee4 |
@@ -202,6 +202,10 @@ get_unwrap = "warn"
|
||||
rc_buffer = "warn"
|
||||
rc_mutex = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
# nursery rules
|
||||
redundant_clone = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
unused_peekable = "warn"
|
||||
|
||||
[profile.release]
|
||||
# Note that we set these explicitly, and these values
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn main() -> ExitStatus {
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let args = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
let args = Args::parse_from(std::env::args());
|
||||
|
||||
if matches!(args.command, Some(Command::Server)) {
|
||||
return run_server().map(|()| ExitStatus::Success);
|
||||
|
||||
@@ -22,3 +22,13 @@ x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` i
|
||||
x: int
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## PEP-604 annotations not yet supported
|
||||
|
||||
```py
|
||||
def f() -> str | None:
|
||||
return None
|
||||
|
||||
# TODO: should be `str | None` (but Todo is better than `Unknown`)
|
||||
reveal_type(f()) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
## Unbound
|
||||
|
||||
```py
|
||||
x = foo
|
||||
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
foo = 1
|
||||
reveal_type(x) # revealed: Unbound
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Unbound class variable
|
||||
@@ -13,6 +16,10 @@ reveal_type(x) # revealed: Unbound
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C:
|
||||
x = 1
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
@@ -21,6 +26,11 @@ reveal_type(f()) # revealed: int | str
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
@@ -34,6 +44,11 @@ reveal_type(f()) # revealed: Unknown | int
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
@@ -50,6 +65,11 @@ reveal_type(x) # revealed: Unknown | int
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
@@ -69,6 +89,11 @@ reveal_type(f())
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
Comparisons on union types need to consider all possible cases:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
one_or_two = 1 if flag else 2
|
||||
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
@@ -52,6 +56,10 @@ With unions on both sides, we need to consider the full cross product of
|
||||
options when building the resulting (union) type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag_s, flag_l = bool_instance(), bool_instance()
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
|
||||
@@ -69,6 +77,10 @@ unsupported. For now, we fall back to `bool` for the result type instead of
|
||||
trying to infer something more precise from the other (supported) variants:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Comparison: Unsupported operators
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
@@ -15,6 +18,7 @@ d = 5 < object()
|
||||
# TODO: should be `Unknown`
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
flag = bool_instance()
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
@@ -10,6 +14,10 @@ reveal_type(x) # revealed: Literal[1, 2]
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
@@ -21,6 +29,10 @@ reveal_type(z) # revealed: Literal[0, 2]
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
@@ -28,6 +40,10 @@ reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
## None
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
@@ -15,6 +19,10 @@ reveal_type(y) # revealed: Literal[2, 3]
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
@@ -28,13 +36,24 @@ else:
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
reveal_type(r) # revealed: Unbound | Literal[2]
|
||||
reveal_type(s) # revealed: Unbound | Literal[5]
|
||||
|
||||
# revealed: Unbound | Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Unbound | Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
@@ -47,6 +66,10 @@ reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
@@ -60,6 +83,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
@@ -74,6 +101,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
if flag2:
|
||||
@@ -84,6 +115,10 @@ reveal_type(y) # revealed: Literal[0, 1]
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
|
||||
@@ -21,7 +21,9 @@ match 0:
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Unbound | Literal[2, 3]
|
||||
# revealed: Unbound | Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
@@ -10,6 +10,10 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
@@ -20,6 +24,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
|
||||
## Partial declarations
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
@@ -28,6 +36,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import re
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except NameError as e:
|
||||
reveal_type(e) # revealed: NameError
|
||||
except re.error as f:
|
||||
@@ -19,7 +19,7 @@ except re.error as f:
|
||||
from nonexistent_module import foo # error: [unresolved-import]
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except foo as e:
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
@@ -31,7 +31,7 @@ except foo as e:
|
||||
EXCEPTIONS = (AttributeError, TypeError)
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except (RuntimeError, OSError) as e:
|
||||
reveal_type(e) # revealed: RuntimeError | OSError
|
||||
except EXCEPTIONS as f:
|
||||
@@ -43,7 +43,7 @@ except EXCEPTIONS as f:
|
||||
```py
|
||||
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
|
||||
try:
|
||||
w
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* BaseException as e:
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
@@ -13,7 +13,7 @@ except* BaseException as e:
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
@@ -23,7 +23,7 @@ except* OSError as e:
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
|
||||
@@ -6,9 +6,13 @@ Basic PEP 695 generics
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
box_model_number = 695
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
@@ -26,13 +30,19 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error on the subscripting
|
||||
class MySecureBox[T](MyBox[T]): ... # error: [non-subscriptable]
|
||||
# TODO not error on the subscripting or the use of type param
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
# error: [non-subscriptable]
|
||||
class MySecureBox[T](MyBox[T]): ...
|
||||
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
|
||||
@@ -3,11 +3,22 @@
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
y = 3
|
||||
x = y
|
||||
reveal_type(x) # revealed: Unbound | Literal[3]
|
||||
reveal_type(y) # revealed: Unbound | Literal[3]
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
```py
|
||||
@@ -20,11 +31,22 @@ reveal_type(y) # revealed: Literal[3]
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
y: int = 3
|
||||
x = y
|
||||
reveal_type(x) # revealed: Unbound | Literal[3]
|
||||
reveal_type(y) # revealed: Unbound | Literal[3]
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
Importing an annotated name prefers the declared type over the inferred type:
|
||||
@@ -43,6 +65,10 @@ def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import f
|
||||
else:
|
||||
@@ -67,6 +93,10 @@ x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import x
|
||||
else:
|
||||
|
||||
@@ -102,7 +102,7 @@ reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
x
|
||||
x # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
|
||||
@@ -17,8 +17,10 @@ async def foo():
|
||||
async for x in Iterator():
|
||||
pass
|
||||
|
||||
# TODO
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
|
||||
# revealed: Unbound | @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Basic async for loop
|
||||
@@ -37,5 +39,7 @@ async def foo():
|
||||
async for x in IntAsyncIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Unbound | @Todo
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
@@ -14,7 +14,9 @@ class IntIterable:
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With previous definition
|
||||
@@ -85,7 +87,9 @@ class OldStyleIterable:
|
||||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With heterogeneous tuple
|
||||
@@ -94,12 +98,19 @@ reveal_type(x) # revealed: Unbound | int
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
@@ -109,7 +120,9 @@ class NotIterable:
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Unknown
|
||||
# revealed: Unbound | Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## Basic While Loop
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
@@ -13,6 +17,10 @@ reveal_type(x) # revealed: Literal[1, 2]
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
@@ -26,6 +34,10 @@ reveal_type(x) # revealed: Literal[3]
|
||||
## While with Else (may break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x is None:
|
||||
@@ -14,6 +18,11 @@ reveal_type(x) # revealed: None | Literal[1]
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
@@ -28,6 +37,10 @@ reveal_type(y) # revealed: A | None
|
||||
## `is` in chained comparisons
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x is not None:
|
||||
@@ -16,6 +20,10 @@ reveal_type(x) # revealed: None | Literal[1]
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
@@ -42,6 +50,10 @@ if x is not y:
|
||||
The type guard removes `False` from the union type of the tested value only.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ if x != 1:
|
||||
## Multiple negative contributions with simplification
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag1, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
if x != 1:
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## `x != None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x != None:
|
||||
@@ -12,6 +16,10 @@ if x != None:
|
||||
## `!=` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
|
||||
if x != False:
|
||||
@@ -21,6 +29,10 @@ if x != False:
|
||||
## `x != y` where `y` is of literal type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
|
||||
if x != 1:
|
||||
@@ -30,6 +42,11 @@ if x != 1:
|
||||
## `x != y` where `y` is a single-valued type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
@@ -44,8 +61,13 @@ if C != A:
|
||||
Only single-valued types should narrow the type:
|
||||
|
||||
```py
|
||||
def int_instance() -> int: ...
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
x = int_instance() if flag else None
|
||||
y = int_instance()
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ Narrowing for `isinstance(object, classinfo)` expressions.
|
||||
## `classinfo` is a single type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
@@ -26,6 +31,11 @@ Note: `isinstance(x, (int, str))` should not be confused with
|
||||
`isinstance(x, int | str)`:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
@@ -56,6 +66,11 @@ if isinstance(y, (str, bytes)):
|
||||
## `classinfo` is a nested tuple of types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
@@ -81,6 +96,11 @@ if isinstance(x, A):
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
t = type("t", (), {})
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
@@ -94,6 +114,11 @@ if isinstance(x, t):
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
|
||||
@@ -105,6 +130,11 @@ if isinstance(x, int):
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
isinstance_alias = isinstance
|
||||
|
||||
x = 1 if flag else "a"
|
||||
@@ -117,6 +147,10 @@ if isinstance_alias(x, int):
|
||||
```py
|
||||
from builtins import isinstance as imported_isinstance
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
@@ -125,6 +159,10 @@ if imported_isinstance(x, int):
|
||||
## Do not narrow if second argument is not a type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
@@ -141,6 +179,10 @@ if isinstance(x, "int"):
|
||||
## Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
## Shadow after incompatible declarations is OK
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
|
||||
@@ -37,6 +37,11 @@ y = 1
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
p = 1
|
||||
q = 3.3
|
||||
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inequality() {
|
||||
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
|
||||
let parsed = ParsedModule::new(parsed_raw.clone());
|
||||
let parsed = ParsedModule::new(parsed_raw);
|
||||
|
||||
let stmt = &parsed.syntax().body[0];
|
||||
let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
|
||||
@@ -150,7 +150,7 @@ mod tests {
|
||||
#[allow(unsafe_code)]
|
||||
fn debug() {
|
||||
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
|
||||
let parsed = ParsedModule::new(parsed_raw.clone());
|
||||
let parsed = ParsedModule::new(parsed_raw);
|
||||
|
||||
let stmt = &parsed.syntax().body[0];
|
||||
|
||||
|
||||
@@ -1294,7 +1294,7 @@ mod tests {
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
custom_typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
@@ -1445,7 +1445,7 @@ mod tests {
|
||||
assert_function_query_was_not_run(
|
||||
&db,
|
||||
resolve_module_query,
|
||||
ModuleNameIngredient::new(&db, functools_module_name.clone()),
|
||||
ModuleNameIngredient::new(&db, functools_module_name),
|
||||
&events,
|
||||
);
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
|
||||
@@ -296,7 +296,7 @@ impl DefinitionNodeRef<'_> {
|
||||
handler,
|
||||
is_star,
|
||||
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef::new(parsed.clone(), handler),
|
||||
handler: AstNodeRef::new(parsed, handler),
|
||||
is_star,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -211,7 +211,13 @@ fn declarations_ty<'db>(
|
||||
let declared_ty = if let Some(second) = all_types.next() {
|
||||
let mut builder = UnionBuilder::new(db).add(first);
|
||||
for other in [second].into_iter().chain(all_types) {
|
||||
if !first.is_equivalent_to(db, other) {
|
||||
// Make sure not to emit spurious errors relating to `Type::Todo`,
|
||||
// since we only infer this type due to a limitation in our current model.
|
||||
//
|
||||
// `Unknown` is different here, since we might infer `Unknown`
|
||||
// for one of these due to a variable being defined in one possible
|
||||
// control-flow branch but not another one.
|
||||
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
|
||||
conflicting.push(other);
|
||||
}
|
||||
builder = builder.add(other);
|
||||
@@ -292,6 +298,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
||||
pub const fn is_todo(&self) -> bool {
|
||||
matches!(self, Type::Todo)
|
||||
}
|
||||
|
||||
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Type::ClassLiteral(class_type) => Some(class_type),
|
||||
|
||||
@@ -320,9 +320,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
db,
|
||||
index,
|
||||
region,
|
||||
|
||||
file,
|
||||
|
||||
types: TypeInference::empty(scope),
|
||||
}
|
||||
}
|
||||
@@ -2418,7 +2416,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
None
|
||||
};
|
||||
|
||||
bindings_ty(self.db, definitions, unbound_ty)
|
||||
let ty = bindings_ty(self.db, definitions, unbound_ty);
|
||||
|
||||
if ty.is_unbound() {
|
||||
self.add_diagnostic(
|
||||
name.into(),
|
||||
"unresolved-reference",
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
} else if ty.may_be_unbound(self.db) {
|
||||
self.add_diagnostic(
|
||||
name.into(),
|
||||
"possibly-unresolved-reference",
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
ty
|
||||
}
|
||||
ExprContext::Store | ExprContext::Del => Type::None,
|
||||
ExprContext::Invalid => Type::Unknown,
|
||||
@@ -3471,6 +3485,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Todo
|
||||
}
|
||||
|
||||
// TODO PEP-604 unions
|
||||
ast::Expr::BinOp(binary) => {
|
||||
self.infer_binary_expression(binary);
|
||||
match binary.op {
|
||||
// PEP-604 unions are okay
|
||||
ast::Operator::BitOr => Type::Todo,
|
||||
// anything else is an invalid annotation:
|
||||
_ => Type::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
// Forms which are invalid in the context of annotation expressions: we infer their
|
||||
// nested expressions as normal expressions, but the type of the top-level expression is
|
||||
// always `Type::Unknown` in these cases.
|
||||
@@ -3482,10 +3507,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_named_expression(named);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::BinOp(binary) => {
|
||||
self.infer_binary_expression(binary);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::UnaryOp(unary) => {
|
||||
self.infer_unary_expression(unary);
|
||||
Type::Unknown
|
||||
@@ -3801,6 +3822,7 @@ mod tests {
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
|
||||
let file = system_path_to_file(db, file_name).expect("file to exist");
|
||||
|
||||
@@ -3812,6 +3834,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_scope_ty(
|
||||
db: &TestDb,
|
||||
file_name: &str,
|
||||
@@ -3837,6 +3860,7 @@ mod tests {
|
||||
assert_eq!(ty.display(db).to_string(), expected);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
|
||||
let messages: Vec<&str> = diagnostics
|
||||
.iter()
|
||||
@@ -3845,6 +3869,7 @@ mod tests {
|
||||
assert_eq!(&messages, expected);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) {
|
||||
let file = system_path_to_file(db, filename).unwrap();
|
||||
let diagnostics = check_types(db, file);
|
||||
@@ -4432,7 +4457,7 @@ mod tests {
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
x
|
||||
print
|
||||
except as e:
|
||||
reveal_type(e)
|
||||
",
|
||||
@@ -4569,7 +4594,10 @@ mod tests {
|
||||
assert_file_diagnostics(
|
||||
&db,
|
||||
"src/a.py",
|
||||
&["Object of type `Unbound` is not iterable"],
|
||||
&[
|
||||
"Name `x` used when not defined",
|
||||
"Object of type `Unbound` is not iterable",
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -4704,7 +4732,7 @@ mod tests {
|
||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unbound");
|
||||
|
||||
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
|
||||
assert_file_diagnostics(&db, "src/a.py", &[]);
|
||||
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ impl<I, T: DoubleEndedIterator<Item = I>> PythonSubscript for T {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::redundant_clone)]
|
||||
mod tests {
|
||||
use super::PythonSubscript;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||
params.cell_text_documents,
|
||||
)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
|
||||
session.open_notebook_document(params.notebook_document.uri, notebook);
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
|
||||
@@ -110,14 +110,14 @@ impl Workspace {
|
||||
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check_file(file_id.file).map_err(into_error)?;
|
||||
|
||||
Ok(result.clone())
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Checks all open files
|
||||
pub fn check(&self) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check().map_err(into_error)?;
|
||||
|
||||
Ok(result.clone())
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns the parsed AST for `path`
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod db;
|
||||
pub mod lint;
|
||||
pub mod watch;
|
||||
pub mod workspace;
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::debug_span;
|
||||
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasTy, ModuleName, SemanticModel};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_db::source::{source_text, SourceText};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::db::Db;
|
||||
|
||||
/// Workaround query to test for if the computation should be cancelled.
|
||||
/// Ideally, push for Salsa to expose an API for testing if cancellation was requested.
|
||||
#[salsa::tracked]
|
||||
#[allow(unused_variables)]
|
||||
pub(crate) fn unwind_if_cancelled(db: &dyn Db) {}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Vec<String> {
|
||||
#[allow(clippy::print_stdout)]
|
||||
if std::env::var("RED_KNOT_SLOW_LINT").is_ok() {
|
||||
for i in 0..10 {
|
||||
unwind_if_cancelled(db);
|
||||
|
||||
println!("RED_KNOT_SLOW_LINT is set, sleeping for {i}/10 seconds");
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let source = source_text(db.upcast(), file_id);
|
||||
lint_lines(&source, &mut diagnostics);
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file_id);
|
||||
|
||||
if parsed.errors().is_empty() {
|
||||
let ast = parsed.syntax();
|
||||
|
||||
let mut visitor = SyntaxLintVisitor {
|
||||
diagnostics,
|
||||
source: &source,
|
||||
};
|
||||
visitor.visit_body(&ast.body);
|
||||
diagnostics = visitor.diagnostics;
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn lint_lines(source: &str, diagnostics: &mut Vec<String>) {
|
||||
for (line_number, line) in source.lines().enumerate() {
|
||||
if line.len() < 88 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let char_count = line.chars().count();
|
||||
if char_count > 88 {
|
||||
diagnostics.push(format!(
|
||||
"Line {} is too long ({} characters)",
|
||||
line_number + 1,
|
||||
char_count
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_pub)]
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub fn lint_semantic(db: &dyn Db, file_id: File) -> Vec<String> {
|
||||
let _span = debug_span!("lint_semantic", file=%file_id.path(db)).entered();
|
||||
|
||||
let source = source_text(db.upcast(), file_id);
|
||||
let parsed = parsed_module(db.upcast(), file_id);
|
||||
let semantic = SemanticModel::new(db.upcast(), file_id);
|
||||
|
||||
if !parsed.is_valid() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let context = SemanticLintContext {
|
||||
source,
|
||||
parsed,
|
||||
semantic,
|
||||
diagnostics: RefCell::new(Vec::new()),
|
||||
};
|
||||
|
||||
SemanticVisitor { context: &context }.visit_body(parsed.suite());
|
||||
|
||||
context.diagnostics.take()
|
||||
}
|
||||
|
||||
fn format_diagnostic(context: &SemanticLintContext, message: &str, start: TextSize) -> String {
|
||||
let source_location = context
|
||||
.semantic
|
||||
.line_index()
|
||||
.source_location(start, context.source_text());
|
||||
format!(
|
||||
"{}:{}:{}: {}",
|
||||
context.semantic.file_path(),
|
||||
source_location.row,
|
||||
source_location.column,
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
fn lint_maybe_undefined(context: &SemanticLintContext, name: &ast::ExprName) {
|
||||
if !matches!(name.ctx, ast::ExprContext::Load) {
|
||||
return;
|
||||
}
|
||||
let semantic = &context.semantic;
|
||||
let ty = name.ty(semantic);
|
||||
if ty.is_unbound() {
|
||||
context.push_diagnostic(format_diagnostic(
|
||||
context,
|
||||
&format!("Name `{}` used when not defined", &name.id),
|
||||
name.start(),
|
||||
));
|
||||
} else if ty.may_be_unbound(semantic.db()) {
|
||||
context.push_diagnostic(format_diagnostic(
|
||||
context,
|
||||
&format!("Name `{}` used when possibly not defined", &name.id),
|
||||
name.start(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn lint_bad_override(context: &SemanticLintContext, class: &ast::StmtClassDef) {
|
||||
let semantic = &context.semantic;
|
||||
|
||||
// TODO we should have a special marker on the real typing module (from typeshed) so if you
|
||||
// have your own "typing" module in your project, we don't consider it THE typing module (and
|
||||
// same for other stdlib modules that our lint rules care about)
|
||||
let Some(typing) = semantic.resolve_module(&ModuleName::new("typing").unwrap()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let override_ty = semantic.global_symbol_ty(&typing, "override");
|
||||
|
||||
let Type::ClassLiteral(class_ty) = class.ty(semantic) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for function in class
|
||||
.body
|
||||
.iter()
|
||||
.filter_map(|stmt| stmt.as_function_def_stmt())
|
||||
{
|
||||
let Type::FunctionLiteral(ty) = function.ty(semantic) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO this shouldn't make direct use of the Db; see comment on SemanticModel::db
|
||||
let db = semantic.db();
|
||||
|
||||
if ty.has_decorator(db, override_ty) {
|
||||
let method_name = ty.name(db);
|
||||
if class_ty
|
||||
.inherited_class_member(db, method_name)
|
||||
.is_unbound()
|
||||
{
|
||||
// TODO should have a qualname() method to support nested classes
|
||||
context.push_diagnostic(
|
||||
format!(
|
||||
"Method {}.{} is decorated with `typing.override` but does not override any base class method",
|
||||
class_ty.name(db),
|
||||
method_name,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SemanticLintContext<'a> {
|
||||
source: SourceText,
|
||||
parsed: &'a ParsedModule,
|
||||
semantic: SemanticModel<'a>,
|
||||
diagnostics: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticLintContext<'db> {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn source_text(&self) -> &str {
|
||||
self.source.as_str()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn ast(&self) -> &'db ast::ModModule {
|
||||
self.parsed.syntax()
|
||||
}
|
||||
|
||||
pub(crate) fn push_diagnostic(&self, diagnostic: String) {
|
||||
self.diagnostics.borrow_mut().push(diagnostic);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator<Item = String>) {
|
||||
self.diagnostics.get_mut().extend(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SyntaxLintVisitor<'a> {
|
||||
diagnostics: Vec<String>,
|
||||
source: &'a str,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for SyntaxLintVisitor<'_> {
|
||||
fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) {
|
||||
// A very naive implementation of use double quotes
|
||||
let text = &self.source[string_literal.range];
|
||||
|
||||
if text.starts_with('\'') {
|
||||
self.diagnostics
|
||||
.push("Use double quotes for strings".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SemanticVisitor<'a> {
|
||||
context: &'a SemanticLintContext<'a>,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for SemanticVisitor<'_> {
|
||||
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
|
||||
if let ast::Stmt::ClassDef(class) = stmt {
|
||||
lint_bad_override(self.context, class);
|
||||
}
|
||||
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &ast::Expr) {
|
||||
match expr {
|
||||
ast::Expr::Name(name) if matches!(name.ctx, ast::ExprContext::Load) => {
|
||||
lint_maybe_undefined(self.context, name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use red_knot_python_semantic::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use super::lint_semantic;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
setup_db_with_root(SystemPathBuf::from("/src"))
|
||||
}
|
||||
|
||||
fn setup_db_with_root(src_root: SystemPathBuf) -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid program settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_variable() {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
x = int
|
||||
if flag:
|
||||
y = x
|
||||
y
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "/src/a.py").expect("file to exist");
|
||||
let messages = lint_semantic(&db, file);
|
||||
|
||||
assert_ne!(messages, &[] as &[String], "expected some diagnostics");
|
||||
|
||||
assert_eq!(
|
||||
*messages,
|
||||
if cfg!(windows) {
|
||||
vec![
|
||||
"\\src\\a.py:3:4: Name `flag` used when not defined",
|
||||
"\\src\\a.py:5:1: Name `y` used when possibly not defined",
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
"/src/a.py:3:4: Name `flag` used when not defined",
|
||||
"/src/a.py:5:1: Name `y` used when possibly not defined",
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,9 @@ use ruff_db::{
|
||||
use ruff_python_ast::{name::Name, PySourceType};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
use crate::{
|
||||
db::Db,
|
||||
lint::{lint_semantic, lint_syntax},
|
||||
};
|
||||
|
||||
mod files;
|
||||
mod metadata;
|
||||
@@ -423,8 +420,6 @@ pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
|
||||
));
|
||||
}
|
||||
|
||||
diagnostics.extend_from_slice(lint_syntax(db, file));
|
||||
diagnostics.extend_from_slice(lint_semantic(db, file));
|
||||
diagnostics
|
||||
}
|
||||
|
||||
@@ -540,17 +535,17 @@ impl Iterator for WorkspaceFilesIter<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::lint::lint_syntax;
|
||||
use crate::workspace::check_file;
|
||||
|
||||
#[test]
|
||||
fn check_file_skips_linting_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = SystemPath::new("test.py");
|
||||
|
||||
@@ -568,7 +563,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run(&db, lint_syntax, file, &events);
|
||||
assert_function_query_was_not_run(&db, check_types, file, &events);
|
||||
|
||||
// The user now creates a new file with an empty text. The source text
|
||||
// content returned by `source_text` remains unchanged, but the diagnostics should get updated.
|
||||
|
||||
@@ -28,30 +28,18 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
// We don't support `*` imports yet:
|
||||
"/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`",
|
||||
// We don't support terminal statements in control flow yet:
|
||||
"/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
|
||||
"/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
|
||||
// True positives!
|
||||
"Line 69 is too long (89 characters)",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
// We don't support terminal statements in control flow yet:
|
||||
"/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:126:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:348:20: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:353:5: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
|
||||
"/src/tomllib/_parser.py:453:24: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:455:9: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
|
||||
"/src/tomllib/_parser.py:482:16: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:566:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:573:12: Name `char` used when possibly not defined",
|
||||
|
||||
9
crates/ruff_linter/resources/test/fixtures/pyflakes/F811_class_fields.py
vendored
Normal file
9
crates/ruff_linter/resources/test/fixtures/pyflakes/F811_class_fields.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Regression test for: https://github.com/astral-sh/ruff/issues/13930"""
|
||||
|
||||
from queue import Empty
|
||||
|
||||
class Types:
|
||||
INVALID = 0
|
||||
UINT = 1
|
||||
HEX = 2
|
||||
Empty = 3
|
||||
@@ -261,6 +261,14 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the re-definition is a class member, abort.
|
||||
// from bar import foo
|
||||
// class Test:
|
||||
// bar = 10 # Okay
|
||||
if scope.kind.is_class() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the bindings are in different forks, abort.
|
||||
|
||||
@@ -88,7 +88,7 @@ where
|
||||
let line_end = locator.full_line_end(script_start.end());
|
||||
let rest = locator.after(line_end);
|
||||
let mut end_offset = None;
|
||||
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end).peekable();
|
||||
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end);
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
let Some(content) = script_line_content(&line) else {
|
||||
|
||||
@@ -1850,7 +1850,7 @@ static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());
|
||||
|
||||
fn args_section(context: &SectionContext) -> FxHashSet<String> {
|
||||
let mut following_lines = context.following_lines().peekable();
|
||||
let mut following_lines = context.following_lines();
|
||||
let Some(first_line) = following_lines.next() else {
|
||||
return FxHashSet::default();
|
||||
};
|
||||
|
||||
@@ -129,6 +129,7 @@ mod tests {
|
||||
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_29.pyi"))]
|
||||
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_30.py"))]
|
||||
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_31.py"))]
|
||||
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_class_fields.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_0.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_1.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_2.py"))]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -122,10 +122,7 @@ impl TestRule for StableTestRuleSafeFix {
|
||||
} else {
|
||||
Some(
|
||||
Diagnostic::new(StableTestRuleSafeFix, ruff_text_size::TextRange::default())
|
||||
.with_fix(Fix::safe_edit(Edit::insertion(
|
||||
comment.to_string(),
|
||||
TextSize::new(0),
|
||||
))),
|
||||
.with_fix(Fix::safe_edit(Edit::insertion(comment, TextSize::new(0)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -169,10 +166,7 @@ impl TestRule for StableTestRuleUnsafeFix {
|
||||
StableTestRuleUnsafeFix,
|
||||
ruff_text_size::TextRange::default(),
|
||||
)
|
||||
.with_fix(Fix::unsafe_edit(Edit::insertion(
|
||||
comment.to_string(),
|
||||
TextSize::new(0),
|
||||
))),
|
||||
.with_fix(Fix::unsafe_edit(Edit::insertion(comment, TextSize::new(0)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -217,7 +211,7 @@ impl TestRule for StableTestRuleDisplayOnlyFix {
|
||||
ruff_text_size::TextRange::default(),
|
||||
)
|
||||
.with_fix(Fix::display_only_edit(Edit::insertion(
|
||||
comment.to_string(),
|
||||
comment,
|
||||
TextSize::new(0),
|
||||
))),
|
||||
)
|
||||
|
||||
@@ -60,15 +60,13 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||
}
|
||||
}
|
||||
|
||||
let docs: Vec<&Attribute> = struct_attributes
|
||||
let docs = struct_attributes
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("doc"))
|
||||
.collect();
|
||||
.filter(|attr| attr.path().is_ident("doc"));
|
||||
|
||||
// Convert the list of `doc` attributes into a single string.
|
||||
let doc = dedent(
|
||||
&docs
|
||||
.into_iter()
|
||||
.map(parse_doc)
|
||||
.collect::<syn::Result<Vec<_>>>()?
|
||||
.join("\n"),
|
||||
|
||||
@@ -30,7 +30,10 @@ pub fn find_only_token_in_range(
|
||||
let token = tokens.next().expect("Expected a token");
|
||||
debug_assert_eq!(token.kind(), token_kind);
|
||||
let mut tokens = tokens.skip_while(|token| token.kind == SimpleTokenKind::LParen);
|
||||
debug_assert_eq!(tokens.next(), None);
|
||||
#[allow(clippy::debug_assert_with_mut_call)]
|
||||
{
|
||||
debug_assert_eq!(tokens.next(), None);
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ fn super_resolution_overview() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let snapshot = session.take_snapshot(file_url.clone()).unwrap();
|
||||
let snapshot = session.take_snapshot(file_url).unwrap();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"changed_notebook",
|
||||
|
||||
@@ -2029,11 +2029,11 @@ mod tests {
|
||||
assert_override(
|
||||
vec![
|
||||
RuleSelection {
|
||||
select: Some(vec![d417.clone()]),
|
||||
select: Some(vec![d417]),
|
||||
..RuleSelection::default()
|
||||
},
|
||||
RuleSelection {
|
||||
extend_select: vec![d41.clone()],
|
||||
extend_select: vec![d41],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
],
|
||||
|
||||
@@ -154,12 +154,12 @@ class Venv:
|
||||
self.path = path
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""The name of the virtual environment directory."""
|
||||
return self.path.name
|
||||
|
||||
@property
|
||||
def python(self):
|
||||
def python(self) -> Path:
|
||||
"""Returns the path to the python executable"""
|
||||
return self.script("python")
|
||||
|
||||
@@ -193,7 +193,7 @@ class Venv:
|
||||
root = parent / "venv"
|
||||
return Venv(root)
|
||||
|
||||
def install(self, dependencies: list[str]):
|
||||
def install(self, dependencies: list[str]) -> None:
|
||||
"""Installs the dependencies required to type check the project."""
|
||||
|
||||
logging.debug(f"Installing dependencies: {', '.join(dependencies)}")
|
||||
@@ -202,7 +202,7 @@ class Venv:
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
self.python,
|
||||
self.python.as_posix(),
|
||||
"--quiet",
|
||||
# We pass `--exclude-newer` to ensure that type-checking of one of
|
||||
# our projects isn't unexpectedly broken by a change in the
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import typing
|
||||
from pathlib import Path
|
||||
@@ -30,9 +29,9 @@ class Project(typing.NamedTuple):
|
||||
mypy_arguments: list[str] | None = None
|
||||
"""The arguments passed to mypy. Overrides `include` if set."""
|
||||
|
||||
def clone(self, checkout_dir: Path):
|
||||
def clone(self, checkout_dir: Path) -> None:
|
||||
# Skip cloning if the project has already been cloned (the script doesn't yet support updating)
|
||||
if os.path.exists(os.path.join(checkout_dir, ".git")):
|
||||
if (checkout_dir / ".git").exists():
|
||||
return
|
||||
|
||||
logging.debug(f"Cloning {self.repository} to {checkout_dir}")
|
||||
|
||||
@@ -16,7 +16,7 @@ if typing.TYPE_CHECKING:
|
||||
from benchmark.cases import Tool
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
"""Run the benchmark."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Benchmark knot against other packaging tools."
|
||||
@@ -69,7 +69,7 @@ def main():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--knot-path",
|
||||
type=str,
|
||||
type=Path,
|
||||
help="Path(s) to the red_knot binary to benchmark.",
|
||||
action="append",
|
||||
)
|
||||
@@ -116,8 +116,8 @@ def main():
|
||||
]
|
||||
|
||||
for project in projects:
|
||||
with tempfile.TemporaryDirectory() as cwd:
|
||||
cwd = Path(cwd)
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
cwd = Path(tempdir)
|
||||
project.clone(cwd)
|
||||
|
||||
venv = Venv.create(cwd)
|
||||
|
||||
Reference in New Issue
Block a user