Compare commits
21 Commits
micha/redu
...
micha/shri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c13a1814f7 | ||
|
|
93aff36147 | ||
|
|
df45a9db64 | ||
|
|
3c69b685ee | ||
|
|
171facd960 | ||
|
|
977447f9b8 | ||
|
|
b3e99b25bf | ||
|
|
dcabb948f3 | ||
|
|
219712860c | ||
|
|
f58a54f043 | ||
|
|
fa28dc5ccf | ||
|
|
63dd68e0ed | ||
|
|
60b3ef2c98 | ||
|
|
3d0a58eb60 | ||
|
|
1db8392a5a | ||
|
|
81e202ed52 | ||
|
|
63c67750b1 | ||
|
|
0a75a1d56b | ||
|
|
bb15c7653a | ||
|
|
cb8b23d609 | ||
|
|
be49151a3d |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -712,7 +712,7 @@ jobs:
|
||||
just test
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2414,6 +2414,7 @@ dependencies = [
|
||||
"red_knot_server",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_trivia",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
@@ -2563,9 +2564,9 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
@@ -2726,7 +2727,6 @@ dependencies = [
|
||||
"mimalloc",
|
||||
"rayon",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
"ruff_python_ast",
|
||||
|
||||
@@ -16,6 +16,7 @@ red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true, features = ["zstd"] }
|
||||
red_knot_server = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
@@ -40,7 +40,7 @@ impl std::fmt::Display for PythonVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
|
||||
impl From<PythonVersion> for ruff_python_ast::python_version::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
@@ -61,8 +61,8 @@ mod tests {
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
|
||||
red_knot_python_semantic::PythonVersion::default()
|
||||
ruff_python_ast::python_version::PythonVersion::from(PythonVersion::default()),
|
||||
ruff_python_ast::python_version::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ use red_knot_project::metadata::pyproject::{PyProject, Tool};
|
||||
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
|
||||
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{
|
||||
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
struct TestCase {
|
||||
db: ProjectDatabase,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
|
||||
use red_knot_python_semantic::{PythonPlatform, SitePackages};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
/// Combine two values, preferring the values in `self`.
|
||||
///
|
||||
|
||||
@@ -309,8 +309,8 @@ mod tests {
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use insta::assert_ron_snapshot;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
use red_knot_python_semantic::{
|
||||
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
|
||||
};
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonPlatform, SearchPathSettings, SitePackages};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::metadata::options::Options;
|
||||
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
|
||||
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::Bound;
|
||||
use std::ops::Deref;
|
||||
|
||||
@@ -57,7 +57,7 @@ quickcheck = { version = "1.0.3", default-features = false }
|
||||
quickcheck_macros = { version = "1.0.0" }
|
||||
|
||||
[features]
|
||||
serde = ["ruff_db/serde", "dep:serde"]
|
||||
serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Special cases for int/float/complex in annotations
|
||||
|
||||
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
|
||||
annotation of `complex` actually means `int | float | complex`. See
|
||||
[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
|
||||
|
||||
## float
|
||||
|
||||
An annotation of `float` means `int | float`, so `int` is assignable to it:
|
||||
|
||||
```py
|
||||
def takes_float(x: float):
|
||||
pass
|
||||
|
||||
def passes_int_to_float(x: int):
|
||||
# no error!
|
||||
takes_float(x)
|
||||
```
|
||||
|
||||
It also applies to variable annotations:
|
||||
|
||||
```py
|
||||
def assigns_int_to_float(x: int):
|
||||
# no error!
|
||||
y: float = x
|
||||
```
|
||||
|
||||
It doesn't work the other way around:
|
||||
|
||||
```py
|
||||
def takes_int(x: int):
|
||||
pass
|
||||
|
||||
def passes_float_to_int(x: float):
|
||||
# error: [invalid-argument-type]
|
||||
takes_int(x)
|
||||
|
||||
def assigns_float_to_int(x: float):
|
||||
# error: [invalid-assignment]
|
||||
y: int = x
|
||||
```
|
||||
|
||||
Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float`
|
||||
as just `float`; we display the actual type:
|
||||
|
||||
```py
|
||||
def f(x: float):
|
||||
reveal_type(x) # revealed: int | float
|
||||
```
|
||||
|
||||
## complex
|
||||
|
||||
An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable
|
||||
to it (but not the other way around):
|
||||
|
||||
```py
|
||||
def takes_complex(x: complex):
|
||||
pass
|
||||
|
||||
def passes_to_complex(x: float, y: int):
|
||||
# no errors!
|
||||
takes_complex(x)
|
||||
takes_complex(y)
|
||||
|
||||
def assigns_to_complex(x: float, y: int):
|
||||
# no errors!
|
||||
a: complex = x
|
||||
b: complex = y
|
||||
|
||||
def takes_int(x: int):
|
||||
pass
|
||||
|
||||
def takes_float(x: float):
|
||||
pass
|
||||
|
||||
def passes_complex(x: complex):
|
||||
# error: [invalid-argument-type]
|
||||
takes_int(x)
|
||||
# error: [invalid-argument-type]
|
||||
takes_float(x)
|
||||
|
||||
def assigns_complex(x: complex):
|
||||
# error: [invalid-assignment]
|
||||
y: int = x
|
||||
# error: [invalid-assignment]
|
||||
z: float = x
|
||||
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
```
|
||||
@@ -9,9 +9,9 @@ from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a1: Union[int, bool]
|
||||
a2: Union[int, Union[float, str]]
|
||||
a2: Union[int, Union[bytes, str]]
|
||||
a3: Union[int, None]
|
||||
a4: Union[Union[float, str]]
|
||||
a4: Union[Union[bytes, str]]
|
||||
a5: Union[int]
|
||||
a6: Union[()]
|
||||
|
||||
@@ -21,11 +21,11 @@ def f():
|
||||
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
|
||||
# revealed: int
|
||||
reveal_type(a1)
|
||||
# revealed: int | float | str
|
||||
# revealed: int | bytes | str
|
||||
reveal_type(a2)
|
||||
# revealed: int | None
|
||||
reveal_type(a3)
|
||||
# revealed: float | str
|
||||
# revealed: bytes | str
|
||||
reveal_type(a4)
|
||||
# revealed: int
|
||||
reveal_type(a5)
|
||||
|
||||
@@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
x = 1.0
|
||||
x /= 2
|
||||
reveal_type(x) # revealed: float
|
||||
reveal_type(x) # revealed: int | float
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
@@ -24,12 +24,12 @@ x -= 1
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
class C:
|
||||
def __iadd__(self, other: str) -> float:
|
||||
return 1.0
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 1
|
||||
|
||||
x = C()
|
||||
x += "Hello"
|
||||
reveal_type(x) # revealed: float
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Unsupported types
|
||||
@@ -130,10 +130,10 @@ def _(flag: bool):
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f = 42
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
reveal_type(f) # revealed: str | Literal[54]
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
@@ -804,6 +804,67 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass but not on a superclass
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
x = 1
|
||||
|
||||
class Bar(Foo):
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass and on a superclass
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
x = 1
|
||||
|
||||
class Bar(Foo):
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
```
|
||||
|
||||
### Attribute access on `Any`
|
||||
|
||||
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
|
||||
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
|
||||
`object` -- but if the attribute *does* exist on `object`, the type of the attribute is
|
||||
`<type as it exists on object> & Any`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Foo(Any): ...
|
||||
|
||||
reveal_type(Foo.bar) # revealed: Any
|
||||
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
|
||||
```
|
||||
|
||||
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
|
||||
|
||||
```py
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
class A:
|
||||
x: ClassVar[Literal[1]] = 1
|
||||
|
||||
class B(Any): ...
|
||||
class C(B, A): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]]
|
||||
reveal_type(C.x) # revealed: Literal[1] & Any
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
@@ -56,7 +56,7 @@ def _(a: bool):
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x / a) # revealed: int | float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_int(x: int):
|
||||
@@ -64,7 +64,7 @@ def _(a: bool):
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a / x) # revealed: int | float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def lhs_is_bool(x: bool):
|
||||
@@ -72,7 +72,7 @@ def _(a: bool):
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x / a) # revealed: int | float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_bool(x: bool):
|
||||
@@ -80,7 +80,7 @@ def _(a: bool):
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a / x) # revealed: int | float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
@@ -88,6 +88,6 @@ def _(a: bool):
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: float
|
||||
reveal_type(x / y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int
|
||||
```
|
||||
|
||||
@@ -268,23 +268,28 @@ reveal_type(B() + B()) # revealed: Unknown | int
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
||||
We get less precise results from binary operations on float/complex literals due to the special case
|
||||
for annotations of `float` or `complex`, which applies also to return annotations for typeshed
|
||||
dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed
|
||||
return annotations from the widening, and preserve a bit more precision here?
|
||||
|
||||
```py
|
||||
reveal_type(3j + 3.14) # revealed: complex
|
||||
reveal_type(4.2 + 42) # revealed: float
|
||||
reveal_type(3j + 3) # revealed: complex
|
||||
reveal_type(3j + 3.14) # revealed: int | float | complex
|
||||
reveal_type(4.2 + 42) # revealed: int | float
|
||||
reveal_type(3j + 3) # revealed: int | float | complex
|
||||
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3.14 + 3j) # revealed: float
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3.14 + 3j) # revealed: int | float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(42 + 4.2) # revealed: int
|
||||
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
reveal_type(4.2 + x) # revealed: int | float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
|
||||
@@ -19,7 +19,7 @@ def lhs(x: int):
|
||||
reveal_type(x - 4) # revealed: int
|
||||
reveal_type(x * -1) # revealed: int
|
||||
reveal_type(x // 3) # revealed: int
|
||||
reveal_type(x / 3) # revealed: float
|
||||
reveal_type(x / 3) # revealed: int | float
|
||||
reveal_type(x % 3) # revealed: int
|
||||
|
||||
def rhs(x: int):
|
||||
@@ -27,7 +27,7 @@ def rhs(x: int):
|
||||
reveal_type(3 - x) # revealed: int
|
||||
reveal_type(3 * x) # revealed: int
|
||||
reveal_type(-3 // x) # revealed: int
|
||||
reveal_type(-3 / x) # revealed: float
|
||||
reveal_type(-3 / x) # revealed: int | float
|
||||
reveal_type(5 % x) # revealed: int
|
||||
|
||||
def both(x: int):
|
||||
@@ -35,7 +35,7 @@ def both(x: int):
|
||||
reveal_type(x - x) # revealed: int
|
||||
reveal_type(x * x) # revealed: int
|
||||
reveal_type(x // x) # revealed: int
|
||||
reveal_type(x / x) # revealed: float
|
||||
reveal_type(x / x) # revealed: int | float
|
||||
reveal_type(x % x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -80,24 +80,20 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
|
||||
reveal_type(c) # revealed: int
|
||||
|
||||
# error: "Cannot divide object of type `int` by zero"
|
||||
# revealed: float
|
||||
reveal_type(int() / 0)
|
||||
reveal_type(int() / 0) # revealed: int | float
|
||||
|
||||
# error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
# revealed: float
|
||||
reveal_type(1 / False)
|
||||
reveal_type(1 / False) # revealed: float
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
True / False
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
bool(1) / False
|
||||
|
||||
# error: "Cannot divide object of type `float` by zero"
|
||||
# revealed: float
|
||||
reveal_type(1.0 / 0)
|
||||
reveal_type(1.0 / 0) # revealed: int | float
|
||||
|
||||
class MyInt(int): ...
|
||||
|
||||
# No error for a subclass of int
|
||||
# revealed: float
|
||||
reveal_type(MyInt(3) / 0)
|
||||
reveal_type(MyInt(3) / 0) # revealed: int | float
|
||||
```
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
```py
|
||||
class Multiplier:
|
||||
def __init__(self, factor: float):
|
||||
def __init__(self, factor: int):
|
||||
self.factor = factor
|
||||
|
||||
def __call__(self, number: float) -> float:
|
||||
def __call__(self, number: int) -> int:
|
||||
return number * self.factor
|
||||
|
||||
a = Multiplier(2.0)(3.0)
|
||||
reveal_type(a) # revealed: float
|
||||
a = Multiplier(2)(3)
|
||||
reveal_type(a) # revealed: int
|
||||
|
||||
class Unit: ...
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ class A:
|
||||
def __eq__(self, other: A) -> int:
|
||||
return 42
|
||||
|
||||
def __ne__(self, other: A) -> float:
|
||||
return 42.0
|
||||
def __ne__(self, other: A) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def __lt__(self, other: A) -> str:
|
||||
return "42"
|
||||
@@ -36,7 +36,7 @@ class A:
|
||||
return {42}
|
||||
|
||||
reveal_type(A() == A()) # revealed: int
|
||||
reveal_type(A() != A()) # revealed: float
|
||||
reveal_type(A() != A()) # revealed: bytearray
|
||||
reveal_type(A() < A()) # revealed: str
|
||||
reveal_type(A() <= A()) # revealed: bytes
|
||||
reveal_type(A() > A()) # revealed: list
|
||||
@@ -55,8 +55,8 @@ class A:
|
||||
def __eq__(self, other: B) -> int:
|
||||
return 42
|
||||
|
||||
def __ne__(self, other: B) -> float:
|
||||
return 42.0
|
||||
def __ne__(self, other: B) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def __lt__(self, other: B) -> str:
|
||||
return "42"
|
||||
@@ -73,7 +73,7 @@ class A:
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() == B()) # revealed: int
|
||||
reveal_type(A() != B()) # revealed: float
|
||||
reveal_type(A() != B()) # revealed: bytearray
|
||||
reveal_type(A() < B()) # revealed: str
|
||||
reveal_type(A() <= B()) # revealed: bytes
|
||||
reveal_type(A() > B()) # revealed: list
|
||||
@@ -93,8 +93,8 @@ class A:
|
||||
def __eq__(self, other: B) -> int:
|
||||
return 42
|
||||
|
||||
def __ne__(self, other: B) -> float:
|
||||
return 42.0
|
||||
def __ne__(self, other: B) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def __lt__(self, other: B) -> str:
|
||||
return "42"
|
||||
@@ -117,7 +117,7 @@ class B:
|
||||
def __ne__(self, other: str) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: should be `int` and `float`.
|
||||
# TODO: should be `int` and `bytearray`.
|
||||
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
|
||||
#
|
||||
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
|
||||
@@ -136,11 +136,11 @@ class C:
|
||||
def __gt__(self, other: C) -> int:
|
||||
return 42
|
||||
|
||||
def __ge__(self, other: C) -> float:
|
||||
return 42.0
|
||||
def __ge__(self, other: C) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
reveal_type(C() < C()) # revealed: int
|
||||
reveal_type(C() <= C()) # revealed: float
|
||||
reveal_type(C() <= C()) # revealed: bytearray
|
||||
```
|
||||
|
||||
## Reflected Comparisons with Subclasses
|
||||
@@ -175,8 +175,8 @@ class B(A):
|
||||
def __eq__(self, other: A) -> int:
|
||||
return 42
|
||||
|
||||
def __ne__(self, other: A) -> float:
|
||||
return 42.0
|
||||
def __ne__(self, other: A) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def __lt__(self, other: A) -> str:
|
||||
return "42"
|
||||
@@ -191,7 +191,7 @@ class B(A):
|
||||
return {42}
|
||||
|
||||
reveal_type(A() == B()) # revealed: int
|
||||
reveal_type(A() != B()) # revealed: float
|
||||
reveal_type(A() != B()) # revealed: bytearray
|
||||
|
||||
reveal_type(A() < B()) # revealed: list
|
||||
reveal_type(A() <= B()) # revealed: set
|
||||
|
||||
@@ -151,11 +151,11 @@ class A:
|
||||
def __ne__(self, o: object) -> bytes:
|
||||
return b"world"
|
||||
|
||||
def __lt__(self, o: A) -> float:
|
||||
return 3.14
|
||||
def __lt__(self, o: A) -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def __le__(self, o: A) -> complex:
|
||||
return complex(0.5, -0.5)
|
||||
def __le__(self, o: A) -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def __gt__(self, o: A) -> tuple:
|
||||
return (1, 2, 3)
|
||||
@@ -167,8 +167,8 @@ a = (A(), A())
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: float | Literal[False]
|
||||
reveal_type(a <= a) # revealed: complex | Literal[True]
|
||||
reveal_type(a < a) # revealed: bytearray | Literal[False]
|
||||
reveal_type(a <= a) # revealed: memoryview | Literal[True]
|
||||
reveal_type(a > a) # revealed: tuple | Literal[False]
|
||||
reveal_type(a >= a) # revealed: list | Literal[True]
|
||||
|
||||
@@ -187,7 +187,7 @@ class B:
|
||||
def __lt__(self, o: B) -> set:
|
||||
return set()
|
||||
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: bytearray | set | Literal[False]
|
||||
```
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
@@ -303,8 +303,8 @@ An example with multiple `except` branches and a `finally` branch:
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -322,13 +322,13 @@ except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_float()
|
||||
reveal_type(x) # revealed: float
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
|
||||
reveal_type(x) # revealed: str | bool | float
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
|
||||
reveal_type(x) # revealed: str | bool | bytearray
|
||||
|
||||
reveal_type(x) # revealed: str | bool | float
|
||||
reveal_type(x) # revealed: str | bool | bytearray
|
||||
```
|
||||
|
||||
## Combining `except`, `else` and `finally` branches
|
||||
@@ -350,8 +350,8 @@ def could_raise_returns_bool() -> bool:
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -369,13 +369,13 @@ else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_float()
|
||||
reveal_type(x) # revealed: float
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
|
||||
reveal_type(x) # revealed: bool | float
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
|
||||
reveal_type(x) # revealed: bool | bytearray
|
||||
|
||||
reveal_type(x) # revealed: bool | float
|
||||
reveal_type(x) # revealed: bool | bytearray
|
||||
```
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
@@ -403,8 +403,8 @@ except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_float()
|
||||
reveal_type(x) # revealed: float
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_range()
|
||||
@@ -412,10 +412,10 @@ else:
|
||||
x = could_raise_returns_slice()
|
||||
reveal_type(x) # revealed: slice
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
|
||||
reveal_type(x) # revealed: bool | float | slice
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray | range | slice`
|
||||
reveal_type(x) # revealed: bool | bytearray | slice
|
||||
|
||||
reveal_type(x) # revealed: bool | float | slice
|
||||
reveal_type(x) # revealed: bool | bytearray | slice
|
||||
```
|
||||
|
||||
## Nested `try`/`except` blocks
|
||||
@@ -441,8 +441,8 @@ def could_raise_returns_bool() -> bool:
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
def could_raise_returns_property() -> property:
|
||||
return property()
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
@@ -450,8 +450,8 @@ def could_raise_returns_range() -> range:
|
||||
def could_raise_returns_slice() -> slice:
|
||||
return slice(None)
|
||||
|
||||
def could_raise_returns_complex() -> complex:
|
||||
return 3j
|
||||
def could_raise_returns_super() -> super:
|
||||
return super()
|
||||
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
@@ -482,8 +482,8 @@ try:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_float()
|
||||
reveal_type(x) # revealed: float
|
||||
x = could_raise_returns_property()
|
||||
reveal_type(x) # revealed: property
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_range()
|
||||
@@ -491,15 +491,15 @@ try:
|
||||
x = could_raise_returns_slice()
|
||||
reveal_type(x) # revealed: slice
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
|
||||
reveal_type(x) # revealed: bool | float | slice
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | property | range | slice`
|
||||
reveal_type(x) # revealed: bool | property | slice
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice
|
||||
x = could_raise_returns_complex()
|
||||
reveal_type(x) # revealed: complex
|
||||
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice
|
||||
x = could_raise_returns_super()
|
||||
reveal_type(x) # revealed: super
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
else:
|
||||
@@ -509,7 +509,7 @@ else:
|
||||
x = could_raise_returns_Bar()
|
||||
reveal_type(x) # revealed: Bar
|
||||
finally:
|
||||
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar`
|
||||
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice | super | bytearray | Foo | Bar`
|
||||
reveal_type(x) # revealed: bytearray | Bar
|
||||
|
||||
# Either one `except` branch or the `else`
|
||||
@@ -535,8 +535,8 @@ def could_raise_returns_range() -> range:
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -553,12 +553,12 @@ try:
|
||||
reveal_type(x) # revealed: str | bytes
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
x = could_raise_returns_float()
|
||||
reveal_type(x) # revealed: float
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
finally:
|
||||
# TODO: should be `str | bytes | bytearray | float`
|
||||
reveal_type(x) # revealed: bytes | float
|
||||
reveal_type(x) # revealed: bytes | float
|
||||
# TODO: should be `str | bytes | bytearray | memoryview`
|
||||
reveal_type(x) # revealed: bytes | memoryview
|
||||
reveal_type(x) # revealed: bytes | memoryview
|
||||
x = foo
|
||||
reveal_type(x) # revealed: Literal[foo]
|
||||
except:
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
# Import conventions
|
||||
|
||||
This document describes the conventions for importing symbols.
|
||||
|
||||
Reference:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
|
||||
|
||||
## Builtins scope
|
||||
|
||||
When looking up for a name, red knot will fallback to using the builtins scope if the name is not
|
||||
found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the
|
||||
builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not
|
||||
re-exported.
|
||||
|
||||
```py
|
||||
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
|
||||
# available in the builtins scope.
|
||||
|
||||
# error: "Name `Literal` used when not defined"
|
||||
reveal_type(Literal) # revealed: Unknown
|
||||
|
||||
# error: "Name `sys` used when not defined"
|
||||
reveal_type(sys) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Builtins import
|
||||
|
||||
Similarly, trying to import the symbols from the builtins module which aren't re-exported should
|
||||
also raise an error.
|
||||
|
||||
```py
|
||||
# error: "Module `builtins` has no member `Literal`"
|
||||
# error: "Module `builtins` has no member `sys`"
|
||||
from builtins import Literal, sys
|
||||
|
||||
reveal_type(Literal) # revealed: Unknown
|
||||
reveal_type(sys) # revealed: Unknown
|
||||
|
||||
# error: "Module `math` has no member `Iterable`"
|
||||
from math import Iterable
|
||||
|
||||
reveal_type(Iterable) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Re-exported symbols in stub files
|
||||
|
||||
When a symbol is re-exported, importing it should not raise an error. This tests both `import ...`
|
||||
and `from ... import ...` forms.
|
||||
|
||||
Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example,
|
||||
in `import os.path as os.path` the `os.path` is not a valid identifier.
|
||||
|
||||
```py
|
||||
from b import Any, Literal, foo
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
reveal_type(Literal) # revealed: typing.Literal
|
||||
reveal_type(foo) # revealed: <module 'foo'>
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
import foo as foo
|
||||
from typing import Any as Any, Literal as Literal
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Non-exported symbols in stub files
|
||||
|
||||
Here, none of the symbols are being re-exported in the stub file.
|
||||
|
||||
```py
|
||||
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
|
||||
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
|
||||
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
|
||||
from b import foo, Any, Literal
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
reveal_type(Literal) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
import foo
|
||||
from typing import Any, Literal
|
||||
```
|
||||
|
||||
`foo.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
## Nested non-exports
|
||||
|
||||
Here, a chain of modules all don't re-export an import.
|
||||
|
||||
```py
|
||||
# error: "Module `a` has no member `Any`"
|
||||
from a import Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
# error: "Module `b` has no member `Any`"
|
||||
from b import Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
# error: "Module `c` has no member `Any`"
|
||||
from c import Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`c.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
```
|
||||
|
||||
## Nested mixed re-export and not
|
||||
|
||||
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not
|
||||
raise an error at that step in the chain.
|
||||
|
||||
```py
|
||||
# error: "Module `a` has no member `Any`"
|
||||
from a import Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
# error: "Module `c` has no member `Any`"
|
||||
from c import Any as Any
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
```
|
||||
|
||||
`c.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
```
|
||||
|
||||
## Exported as different name
|
||||
|
||||
The re-export convention only works when the aliased name is exactly the same as the original name.
|
||||
|
||||
```py
|
||||
# error: "Module `a` has no member `Foo`"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import AnyFoo as Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[AnyFoo]
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class AnyFoo: ...
|
||||
```
|
||||
|
||||
## Exported using `__all__`
|
||||
|
||||
Here, the symbol is re-exported using the `__all__` variable.
|
||||
|
||||
```py
|
||||
# TODO: This should *not* be an error but we don't understand `__all__` yet.
|
||||
# error: "Module `a` has no member `Foo`"
|
||||
from a import Foo
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import Foo
|
||||
|
||||
__all__ = ['Foo']
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Re-exports in `__init__.pyi`
|
||||
|
||||
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
|
||||
but the inference would be `Unknown`.
|
||||
|
||||
```py
|
||||
# error: 15 "Module `a` has no member `Foo`"
|
||||
# error: 20 "Module `a` has no member `c`"
|
||||
from a import Foo, c, foo
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: <module 'a.foo'>
|
||||
```
|
||||
|
||||
`a/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
from .b import c
|
||||
from .foo import Foo
|
||||
```
|
||||
|
||||
`a/foo.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
`a/b/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`a/b/c.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
## Conditional re-export in stub file
|
||||
|
||||
The following scenarios are when a re-export happens conditionally in a stub file.
|
||||
|
||||
### Global import
|
||||
|
||||
```py
|
||||
# error: "Member `Foo` of module `a` is possibly unbound"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: str
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import Foo
|
||||
|
||||
def coinflip() -> bool: ...
|
||||
|
||||
if coinflip():
|
||||
Foo: str = ...
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo] | str
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
### Both branch is an import
|
||||
|
||||
Here, both the branches of the condition are import statements where one of them re-exports while
|
||||
the other does not.
|
||||
|
||||
```py
|
||||
# error: "Member `Foo` of module `a` is possibly unbound"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
def coinflip() -> bool: ...
|
||||
|
||||
if coinflip():
|
||||
from b import Foo
|
||||
else:
|
||||
from b import Foo as Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
### Re-export in one branch
|
||||
|
||||
```py
|
||||
# error: "Member `Foo` of module `a` is possibly unbound"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
def coinflip() -> bool: ...
|
||||
|
||||
if coinflip():
|
||||
from b import Foo as Foo
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
### Non-export in one branch
|
||||
|
||||
```py
|
||||
# error: "Module `a` has no member `Foo`"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
def coinflip() -> bool: ...
|
||||
|
||||
if coinflip():
|
||||
from b import Foo
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
@@ -11,11 +11,15 @@ See the [typing documentation] for more information.
|
||||
|
||||
- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a
|
||||
supertype of `bool` (present in `bool`s bases and MRO).
|
||||
- `int` is not a subtype of `float`/`complex`, even though `float`/`complex` can be used in place of
|
||||
`int` in some contexts (see [special case for float and complex]).
|
||||
- `int` is not a subtype of `float`/`complex`, although this is muddied by the
|
||||
[special case for float and complex] where annotations of `float` and `complex` are interpreted
|
||||
as `int | float` and `int | float | complex`, respectively.
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
from knot_extensions import is_subtype_of, static_assert, TypeOf
|
||||
|
||||
type JustFloat = TypeOf[1.0]
|
||||
type JustComplex = TypeOf[1j]
|
||||
|
||||
static_assert(is_subtype_of(bool, bool))
|
||||
static_assert(is_subtype_of(bool, int))
|
||||
@@ -30,8 +34,8 @@ static_assert(not is_subtype_of(int, bool))
|
||||
static_assert(not is_subtype_of(int, str))
|
||||
static_assert(not is_subtype_of(object, int))
|
||||
|
||||
static_assert(not is_subtype_of(int, float))
|
||||
static_assert(not is_subtype_of(int, complex))
|
||||
static_assert(not is_subtype_of(int, JustFloat))
|
||||
static_assert(not is_subtype_of(int, JustComplex))
|
||||
|
||||
static_assert(is_subtype_of(TypeError, Exception))
|
||||
static_assert(is_subtype_of(FloatingPointError, Exception))
|
||||
@@ -79,7 +83,9 @@ static_assert(is_subtype_of(C, object))
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
from knot_extensions import is_subtype_of, static_assert, TypeOf
|
||||
|
||||
type JustFloat = TypeOf[1.0]
|
||||
|
||||
# Boolean literals
|
||||
static_assert(is_subtype_of(Literal[True], bool))
|
||||
@@ -92,8 +98,7 @@ static_assert(is_subtype_of(Literal[1], object))
|
||||
|
||||
static_assert(not is_subtype_of(Literal[1], bool))
|
||||
|
||||
# See the note above (or link below) concerning int and float/complex
|
||||
static_assert(not is_subtype_of(Literal[1], float))
|
||||
static_assert(not is_subtype_of(Literal[1], JustFloat))
|
||||
|
||||
# String literals
|
||||
static_assert(is_subtype_of(Literal["foo"], LiteralString))
|
||||
|
||||
@@ -70,11 +70,11 @@ from typing import Literal
|
||||
def _(
|
||||
u1: (int | str) | bytes,
|
||||
u2: int | (str | bytes),
|
||||
u3: int | (str | (bytes | complex)),
|
||||
u3: int | (str | (bytes | bytearray)),
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: int | str | bytes
|
||||
reveal_type(u2) # revealed: int | str | bytes
|
||||
reveal_type(u3) # revealed: int | str | bytes | complex
|
||||
reveal_type(u3) # revealed: int | str | bytes | bytearray
|
||||
```
|
||||
|
||||
## Simplification using subtyping
|
||||
|
||||
@@ -19,7 +19,6 @@ pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
|
||||
|
||||
use super::Db;
|
||||
@@ -29,6 +28,7 @@ pub(crate) mod tests {
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -9,7 +9,6 @@ pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasType, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
@@ -20,7 +19,6 @@ mod module_resolver;
|
||||
mod node_key;
|
||||
mod program;
|
||||
mod python_platform;
|
||||
mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
|
||||
@@ -631,10 +631,10 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::Db;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
|
||||
use crate::site_packages::VirtualEnvironment;
|
||||
use crate::{Program, PythonVersion, SearchPathSettings, SitePackages};
|
||||
use crate::{Program, SearchPathSettings, SitePackages};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
@@ -724,12 +725,12 @@ mod tests {
|
||||
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
|
||||
};
|
||||
use ruff_db::Db;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::module::ModuleKind;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::PythonVersion;
|
||||
use crate::{ProgramSettings, PythonPlatform};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{ProgramSettings, PythonPlatform, SitePackages};
|
||||
|
||||
/// A test case for the module resolver.
|
||||
|
||||
@@ -4,11 +4,12 @@ use std::num::{NonZeroU16, NonZeroUsize};
|
||||
use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::{Program, PythonVersion};
|
||||
use crate::Program;
|
||||
|
||||
pub(in crate::module_resolver) fn vendored_typeshed_versions(db: &dyn Db) -> TypeshedVersions {
|
||||
TypeshedVersions::from_str(
|
||||
@@ -278,12 +279,12 @@ impl FromStr for PyVersionRange {
|
||||
let mut parts = s.split('-').map(str::trim);
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(lower), Some(""), None) => {
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
let lower = python_version_from_versions_file_string(lower)?;
|
||||
Ok(Self::AvailableFrom(lower..))
|
||||
}
|
||||
(Some(lower), Some(upper), None) => {
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
let upper = PythonVersion::from_versions_file_string(upper)?;
|
||||
let lower = python_version_from_versions_file_string(lower)?;
|
||||
let upper = python_version_from_versions_file_string(upper)?;
|
||||
Ok(Self::AvailableWithin(lower..=upper))
|
||||
}
|
||||
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
|
||||
@@ -302,21 +303,21 @@ impl fmt::Display for PyVersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
|
||||
let mut parts = s.split('.').map(str::trim);
|
||||
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
|
||||
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
|
||||
s.to_string(),
|
||||
));
|
||||
};
|
||||
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
|
||||
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err: int_parse_error,
|
||||
}
|
||||
})
|
||||
}
|
||||
fn python_version_from_versions_file_string(
|
||||
s: &str,
|
||||
) -> Result<PythonVersion, TypeshedVersionsParseErrorKind> {
|
||||
let mut parts = s.split('.').map(str::trim);
|
||||
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
|
||||
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
|
||||
s.to_string(),
|
||||
));
|
||||
};
|
||||
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
|
||||
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err: int_parse_error,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::module_resolver::SearchPaths;
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::Db;
|
||||
|
||||
use anyhow::Context;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
use super::definition::{
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, ImportDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
};
|
||||
|
||||
mod except_handlers;
|
||||
@@ -886,22 +886,28 @@ where
|
||||
self.imported_modules.extend(module_name.ancestors());
|
||||
}
|
||||
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
asname.id.clone()
|
||||
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
|
||||
(asname.id.clone(), asname.id == alias.name.id)
|
||||
} else {
|
||||
Name::new(alias.name.id.split('.').next().unwrap())
|
||||
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
||||
};
|
||||
|
||||
let symbol = self.add_symbol(symbol_name);
|
||||
self.add_definition(symbol, alias);
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ImportDefinitionNodeRef {
|
||||
alias,
|
||||
is_reexported,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Stmt::ImportFrom(node) => {
|
||||
for (alias_index, alias) in node.names.iter().enumerate() {
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
&asname.id
|
||||
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
|
||||
(&asname.id, asname.id == alias.name.id)
|
||||
} else {
|
||||
&alias.name.id
|
||||
(&alias.name.id, false)
|
||||
};
|
||||
|
||||
// Look for imports `from __future__ import annotations`, ignore `as ...`
|
||||
@@ -914,7 +920,14 @@ where
|
||||
|
||||
let symbol = self.add_symbol(symbol_name.clone());
|
||||
|
||||
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ImportFromDefinitionNodeRef {
|
||||
node,
|
||||
alias_index,
|
||||
is_reexported,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Assign(node) => {
|
||||
|
||||
@@ -57,11 +57,15 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).is_reexported()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Import(&'a ast::Alias),
|
||||
Import(ImportDefinitionNodeRef<'a>),
|
||||
ImportFrom(ImportFromDefinitionNodeRef<'a>),
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
@@ -119,12 +123,6 @@ impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: &'a ast::Alias) -> Self {
|
||||
Self::Import(node_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> {
|
||||
fn from(value: &'a ast::TypeParamTypeVar) -> Self {
|
||||
Self::TypeVar(value)
|
||||
@@ -143,6 +141,12 @@ impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImportDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: ImportDefinitionNodeRef<'a>) -> Self {
|
||||
Self::Import(node_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self {
|
||||
Self::ImportFrom(node_ref)
|
||||
@@ -185,10 +189,17 @@ impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportDefinitionNodeRef<'a> {
|
||||
pub(crate) alias: &'a ast::Alias,
|
||||
pub(crate) is_reexported: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::StmtImportFrom,
|
||||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -244,15 +255,22 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> {
|
||||
match self {
|
||||
DefinitionNodeRef::Import(alias) => {
|
||||
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
|
||||
}
|
||||
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => {
|
||||
DefinitionKind::ImportFrom(ImportFromDefinitionKind {
|
||||
node: AstNodeRef::new(parsed, node),
|
||||
alias_index,
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Import(ImportDefinitionNodeRef {
|
||||
alias,
|
||||
is_reexported,
|
||||
}) => DefinitionKind::Import(ImportDefinitionKind {
|
||||
alias: AstNodeRef::new(parsed, alias),
|
||||
is_reexported,
|
||||
}),
|
||||
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef {
|
||||
node,
|
||||
alias_index,
|
||||
is_reexported,
|
||||
}) => DefinitionKind::ImportFrom(ImportFromDefinitionKind {
|
||||
node: AstNodeRef::new(parsed, node),
|
||||
alias_index,
|
||||
is_reexported,
|
||||
}),
|
||||
DefinitionNodeRef::Function(function) => {
|
||||
DefinitionKind::Function(AstNodeRef::new(parsed, function))
|
||||
}
|
||||
@@ -354,10 +372,15 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
|
||||
pub(super) fn key(self) -> DefinitionNodeKey {
|
||||
match self {
|
||||
Self::Import(node) => node.into(),
|
||||
Self::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => {
|
||||
(&node.names[alias_index]).into()
|
||||
}
|
||||
Self::Import(ImportDefinitionNodeRef {
|
||||
alias,
|
||||
is_reexported: _,
|
||||
}) => alias.into(),
|
||||
Self::ImportFrom(ImportFromDefinitionNodeRef {
|
||||
node,
|
||||
alias_index,
|
||||
is_reexported: _,
|
||||
}) => (&node.names[alias_index]).into(),
|
||||
Self::Function(node) => node.into(),
|
||||
Self::Class(node) => node.into(),
|
||||
Self::TypeAlias(node) => node.into(),
|
||||
@@ -441,7 +464,7 @@ impl DefinitionCategory {
|
||||
/// for an in-depth explanation of why this is necessary.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DefinitionKind<'db> {
|
||||
Import(AstNodeRef<ast::Alias>),
|
||||
Import(ImportDefinitionKind),
|
||||
ImportFrom(ImportFromDefinitionKind),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
@@ -464,6 +487,14 @@ pub enum DefinitionKind<'db> {
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.is_reexported(),
|
||||
DefinitionKind::ImportFrom(import) => import.is_reexported(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the definition target.
|
||||
///
|
||||
/// A definition target would mainly be the node representing the symbol being defined i.e.,
|
||||
@@ -472,7 +503,7 @@ impl DefinitionKind<'_> {
|
||||
/// This is mainly used for logging and debugging purposes.
|
||||
pub(crate) fn target_range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(alias) => alias.range(),
|
||||
DefinitionKind::Import(import) => import.alias().range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::Function(function) => function.name.range(),
|
||||
DefinitionKind::Class(class) => class.name.range(),
|
||||
@@ -603,10 +634,27 @@ impl ComprehensionDefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImportDefinitionKind {
|
||||
alias: AstNodeRef<ast::Alias>,
|
||||
is_reexported: bool,
|
||||
}
|
||||
|
||||
impl ImportDefinitionKind {
|
||||
pub(crate) fn alias(&self) -> &ast::Alias {
|
||||
self.alias.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImportFromDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImportFrom>,
|
||||
alias_index: usize,
|
||||
is_reexported: bool,
|
||||
}
|
||||
|
||||
impl ImportFromDefinitionKind {
|
||||
@@ -617,6 +665,10 @@ impl ImportFromDefinitionKind {
|
||||
pub(crate) fn alias(&self) -> &ast::Alias {
|
||||
&self.node.node().names[self.alias_index]
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
|
||||
use crate::PythonVersion;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::module_resolver::{resolve_module, KnownModule};
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::symbol::Symbol;
|
||||
use crate::types::global_symbol;
|
||||
use crate::types::imported_symbol;
|
||||
use crate::Db;
|
||||
|
||||
/// Lookup the type of `symbol` in a given known module
|
||||
@@ -14,18 +14,10 @@ pub(crate) fn known_module_symbol<'db>(
|
||||
symbol: &str,
|
||||
) -> Symbol<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| global_symbol(db, module.file(), symbol))
|
||||
.map(|module| imported_symbol(db, &module, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
known_module_symbol(db, KnownModule::Builtins, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||
|
||||
@@ -40,7 +40,7 @@ impl<'db> Symbol<'db> {
|
||||
|
||||
/// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type
|
||||
/// and boundness [`Boundness::Bound`].
|
||||
#[allow(unused_variables)]
|
||||
#[allow(unused_variables)] // Only unused in release builds
|
||||
pub(crate) fn todo(message: &'static str) -> Self {
|
||||
Symbol::Type(todo_type!(message), Boundness::Bound)
|
||||
}
|
||||
@@ -67,6 +67,30 @@ impl<'db> Symbol<'db> {
|
||||
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||
}
|
||||
|
||||
/// Transform the symbol into a [`LookupResult`],
|
||||
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
|
||||
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
|
||||
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, Boundness::Bound) => Ok(ty),
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)),
|
||||
Symbol::Unbound => Err(LookupError::Unbound),
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely unwrap the symbol into a [`Type`].
|
||||
///
|
||||
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
|
||||
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
|
||||
/// the result of `diagnostic_fn` (which will be a [`Type`]). This allows the caller to ensure
|
||||
/// that a diagnostic is emitted if the symbol is possibly or definitely unbound.
|
||||
pub(crate) fn unwrap_with_diagnostic(
|
||||
self,
|
||||
diagnostic_fn: impl FnOnce(LookupError<'db>) -> Type<'db>,
|
||||
) -> Type<'db> {
|
||||
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
|
||||
}
|
||||
|
||||
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
@@ -83,17 +107,9 @@ impl<'db> Symbol<'db> {
|
||||
db: &'db dyn Db,
|
||||
fallback_fn: impl FnOnce() -> Self,
|
||||
) -> Self {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::Bound) => self,
|
||||
Symbol::Unbound => fallback_fn(),
|
||||
Symbol::Type(self_ty, Boundness::PossiblyUnbound) => match fallback_fn() {
|
||||
Symbol::Unbound => self,
|
||||
Symbol::Type(fallback_ty, fallback_boundness) => Symbol::Type(
|
||||
UnionType::from_elements(db, [self_ty, fallback_ty]),
|
||||
fallback_boundness,
|
||||
),
|
||||
},
|
||||
}
|
||||
self.into_lookup_result()
|
||||
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -105,6 +121,51 @@ impl<'db> Symbol<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<LookupResult<'db>> for Symbol<'db> {
|
||||
fn from(value: LookupResult<'db>) -> Self {
|
||||
match value {
|
||||
Ok(ty) => Symbol::Type(ty, Boundness::Bound),
|
||||
Err(LookupError::Unbound) => Symbol::Unbound,
|
||||
Err(LookupError::PossiblyUnbound(ty)) => Symbol::Type(ty, Boundness::PossiblyUnbound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible ways in which a symbol lookup can (possibly or definitely) fail.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum LookupError<'db> {
|
||||
Unbound,
|
||||
PossiblyUnbound(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> LookupError<'db> {
|
||||
/// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`].
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback: Symbol<'db>,
|
||||
) -> LookupResult<'db> {
|
||||
let fallback = fallback.into_lookup_result();
|
||||
match (&self, &fallback) {
|
||||
(LookupError::Unbound, _) => fallback,
|
||||
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound)) => Err(self),
|
||||
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => {
|
||||
Ok(UnionType::from_elements(db, [ty, ty2]))
|
||||
}
|
||||
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => Err(
|
||||
LookupError::PossiblyUnbound(UnionType::from_elements(db, [ty, ty2])),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Result`] type in which the `Ok` variant represents a definitely bound symbol
|
||||
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
|
||||
///
|
||||
/// Note that this type is exactly isomorphic to [`Symbol`].
|
||||
/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead.
|
||||
pub(crate) type LookupResult<'db> = Result<Type<'db>, LookupError<'db>>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -8,6 +8,7 @@ use itertools::Itertools;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use type_ordering::union_elements_ordering;
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
@@ -32,9 +33,9 @@ use crate::semantic_index::{
|
||||
use_def_map, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
|
||||
DeclarationsIterator,
|
||||
};
|
||||
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
|
||||
use crate::stdlib::{known_module_symbol, typing_extensions_symbol};
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::symbol::{Boundness, LookupError, LookupResult, Symbol};
|
||||
use crate::types::call::{
|
||||
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
|
||||
};
|
||||
@@ -43,7 +44,7 @@ use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
|
||||
use crate::{Db, FxOrderSet, Module, Program};
|
||||
|
||||
mod builder;
|
||||
mod call;
|
||||
@@ -106,14 +107,30 @@ fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl RequiresExplicitReExport {
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, RequiresExplicitReExport::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
#[salsa::tracked]
|
||||
fn symbol_by_id<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
is_dunder_slots: bool,
|
||||
symbol_id: ScopedSymbolId,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
let use_def = use_def_map(db, scope);
|
||||
|
||||
@@ -121,7 +138,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// on inference from bindings.
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations(db, declarations);
|
||||
let declared = symbol_from_declarations(db, declarations, requires_explicit_reexport);
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
@@ -131,7 +148,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
let inferred = symbol_from_bindings(db, bindings, requires_explicit_reexport);
|
||||
|
||||
match inferred {
|
||||
// Symbol is possibly undeclared and definitely unbound
|
||||
@@ -151,9 +168,17 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(Symbol::Unbound) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
let inferred = symbol_from_bindings(db, bindings, requires_explicit_reexport);
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_dunder_slots || is_final)
|
||||
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
let is_considered_non_modifiable =
|
||||
is_final || symbol_table(db, scope).symbol(symbol_id).name() == "__slots__";
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable)
|
||||
}
|
||||
// Symbol has conflicting declared types
|
||||
Err((declared_ty, _)) => {
|
||||
@@ -203,16 +228,9 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
}
|
||||
}
|
||||
|
||||
let table = symbol_table(db, scope);
|
||||
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
let is_dunder_slots = name == "__slots__";
|
||||
table
|
||||
symbol_table(db, scope)
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_by_id(db, scope, is_dunder_slots, symbol))
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
@@ -251,23 +269,99 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Looks up a module-global symbol by name in a file.
|
||||
/// Return the symbol for a member of `types.ModuleType`.
|
||||
pub(crate) fn module_type_symbol<'db>(db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope) in the given
|
||||
/// `scope`.
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Infers the public type of a module-global symbol as seen from within the same file.
|
||||
///
|
||||
/// If it's not defined explicitly in the global scope, it will look it up in `types.ModuleType`
|
||||
/// with a few very special exceptions.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
// Not defined explicitly in the global scope?
|
||||
// All modules are instances of `types.ModuleType`;
|
||||
// look it up there (with a few very special exceptions)
|
||||
symbol(db, global_scope(db, file), name).or_fall_back_to(db, || {
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
RequiresExplicitReExport::No,
|
||||
)
|
||||
.or_fall_back_to(db, || module_type_symbol(db, name))
|
||||
}
|
||||
|
||||
/// Infers the public type of an imported symbol.
|
||||
pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str) -> Symbol<'db> {
|
||||
// If it's not found in the global scope, check if it's present as an instance on
|
||||
// `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
// We do a more limited version of this in `global_symbol`, but there are two crucial
|
||||
// differences here:
|
||||
// - If a member is looked up as an attribute, `__init__` is also available on the module, but
|
||||
// it isn't available as a global from inside the module
|
||||
// - If a member is looked up as an attribute, members on `builtins.object` are also available
|
||||
// (because `types.ModuleType` inherits from `object`); these attributes are also not
|
||||
// available as globals from inside the module.
|
||||
//
|
||||
// The same way as in `global_symbol`, however, we need to be careful to ignore
|
||||
// `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
|
||||
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
|
||||
// module we're dealing with.
|
||||
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||
///
|
||||
/// Note that this function is only intended for use in the context of the builtins *namespace*
|
||||
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
|
||||
/// (e.g. `from builtins import int`).
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
resolve_module(db, &KnownModule::Builtins.name())
|
||||
.map(|module| {
|
||||
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
|
||||
// We're looking up in the builtins namespace and not the module, so we should
|
||||
// do the normal lookup in `types.ModuleType` and not the special one as in
|
||||
// `imported_symbol`.
|
||||
module_type_symbol(db, symbol)
|
||||
})
|
||||
})
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
if file.is_stub(db.upcast()) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
|
||||
let inference = infer_definition_types(db, definition);
|
||||
@@ -317,19 +411,24 @@ fn definition_expression_type<'db>(
|
||||
fn symbol_from_bindings<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
let visibility_constraints = bindings_with_constraints.visibility_constraints;
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let unbound_visibility = if let Some(BindingWithConstraints {
|
||||
binding: None,
|
||||
constraints: _,
|
||||
visibility_constraint,
|
||||
}) = bindings_with_constraints.peek()
|
||||
{
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
let is_non_exported = |binding: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !binding.is_reexported(db)
|
||||
};
|
||||
|
||||
let unbound_visibility = match bindings_with_constraints.peek() {
|
||||
Some(BindingWithConstraints {
|
||||
binding,
|
||||
visibility_constraint,
|
||||
constraints: _,
|
||||
}) if binding.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let mut types = bindings_with_constraints.filter_map(
|
||||
@@ -339,6 +438,11 @@ fn symbol_from_bindings<'db>(
|
||||
visibility_constraint,
|
||||
}| {
|
||||
let binding = binding?;
|
||||
|
||||
if is_non_exported(binding) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
@@ -438,18 +542,23 @@ type SymbolFromDeclarationsResult<'db> =
|
||||
fn symbol_from_declarations<'db>(
|
||||
db: &'db dyn Db,
|
||||
declarations: DeclarationsIterator<'_, 'db>,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> SymbolFromDeclarationsResult<'db> {
|
||||
let visibility_constraints = declarations.visibility_constraints;
|
||||
let mut declarations = declarations.peekable();
|
||||
|
||||
let undeclared_visibility = if let Some(DeclarationWithConstraint {
|
||||
declaration: None,
|
||||
visibility_constraint,
|
||||
}) = declarations.peek()
|
||||
{
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !declaration.is_reexported(db)
|
||||
};
|
||||
|
||||
let undeclared_visibility = match declarations.peek() {
|
||||
Some(DeclarationWithConstraint {
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}) if declaration.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let mut types = declarations.filter_map(
|
||||
@@ -458,6 +567,11 @@ fn symbol_from_declarations<'db>(
|
||||
visibility_constraint,
|
||||
}| {
|
||||
let declaration = declaration?;
|
||||
|
||||
if is_non_exported(declaration) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
@@ -1651,6 +1765,7 @@ impl<'db> Type<'db> {
|
||||
| KnownClass::Type
|
||||
| KnownClass::Int
|
||||
| KnownClass::Float
|
||||
| KnownClass::Complex
|
||||
| KnownClass::Str
|
||||
| KnownClass::List
|
||||
| KnownClass::Tuple
|
||||
@@ -1942,7 +2057,7 @@ impl<'db> Type<'db> {
|
||||
fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> {
|
||||
match self {
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(self));
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
|
||||
match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => {
|
||||
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
@@ -2319,6 +2434,31 @@ impl<'db> Type<'db> {
|
||||
db: &'db dyn Db,
|
||||
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
|
||||
match self {
|
||||
// Special cases for `float` and `complex`
|
||||
// https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
Type::ClassLiteral(ClassLiteralType { class })
|
||||
if class.is_known(db, KnownClass::Float) =>
|
||||
{
|
||||
Ok(UnionType::from_elements(
|
||||
db,
|
||||
[
|
||||
KnownClass::Int.to_instance(db),
|
||||
KnownClass::Float.to_instance(db),
|
||||
],
|
||||
))
|
||||
}
|
||||
Type::ClassLiteral(ClassLiteralType { class })
|
||||
if class.is_known(db, KnownClass::Complex) =>
|
||||
{
|
||||
Ok(UnionType::from_elements(
|
||||
db,
|
||||
[
|
||||
KnownClass::Int.to_instance(db),
|
||||
KnownClass::Float.to_instance(db),
|
||||
KnownClass::Complex.to_instance(db),
|
||||
],
|
||||
))
|
||||
}
|
||||
// In a type expression, a bare `type` is interpreted as "instance of `type`", which is
|
||||
// equivalent to `type[object]`.
|
||||
Type::ClassLiteral(_) | Type::SubclassOf(_) => Ok(self.to_instance(db)),
|
||||
@@ -2694,6 +2834,7 @@ pub enum KnownClass {
|
||||
Type,
|
||||
Int,
|
||||
Float,
|
||||
Complex,
|
||||
Str,
|
||||
List,
|
||||
Tuple,
|
||||
@@ -2739,6 +2880,7 @@ impl<'db> KnownClass {
|
||||
Self::Tuple => "tuple",
|
||||
Self::Int => "int",
|
||||
Self::Float => "float",
|
||||
Self::Complex => "complex",
|
||||
Self::FrozenSet => "frozenset",
|
||||
Self::Str => "str",
|
||||
Self::Set => "set",
|
||||
@@ -2808,6 +2950,7 @@ impl<'db> KnownClass {
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
@@ -2857,6 +3000,7 @@ impl<'db> KnownClass {
|
||||
| Self::Tuple
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::Set
|
||||
| Self::FrozenSet
|
||||
@@ -2893,6 +3037,7 @@ impl<'db> KnownClass {
|
||||
"type" => Self::Type,
|
||||
"int" => Self::Int,
|
||||
"float" => Self::Float,
|
||||
"complex" => Self::Complex,
|
||||
"str" => Self::Str,
|
||||
"set" => Self::Set,
|
||||
"frozenset" => Self::FrozenSet,
|
||||
@@ -2932,6 +3077,7 @@ impl<'db> KnownClass {
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
@@ -3795,28 +3941,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not found in the global scope, check if it's present as an instance
|
||||
// on `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
// We do a more limited version of this in `global_symbol_ty`,
|
||||
// but there are two crucial differences here:
|
||||
// - If a member is looked up as an attribute, `__init__` is also available
|
||||
// on the module, but it isn't available as a global from inside the module
|
||||
// - If a member is looked up as an attribute, members on `builtins.object`
|
||||
// are also available (because `types.ModuleType` inherits from `object`);
|
||||
// these attributes are also not available as globals from inside the module.
|
||||
//
|
||||
// The same way as in `global_symbol_ty`, however, we need to be careful to
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
|
||||
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
|
||||
// where we know exactly which module we're dealing with.
|
||||
symbol(db, global_scope(db, self.module(db).file()), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
imported_symbol(db, &self.module(db), name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4127,21 +4252,45 @@ impl<'db> Class<'db> {
|
||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements));
|
||||
}
|
||||
|
||||
// If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
|
||||
// in this variable. After we've traversed the MRO, we'll either:
|
||||
// (1) Use that dynamic type as the type for this attribute,
|
||||
// if no other classes in the MRO define the attribute; or,
|
||||
// (2) Intersect that dynamic type with the type of the attribute
|
||||
// from the non-dynamic members of the class's MRO.
|
||||
let mut dynamic_type_to_intersect_with: Option<Type<'db>> = None;
|
||||
|
||||
let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound);
|
||||
|
||||
for superclass in self.iter_mro(db) {
|
||||
match superclass {
|
||||
// TODO we may instead want to record the fact that we encountered dynamic, and intersect it with
|
||||
// the type found on the next "real" class.
|
||||
ClassBase::Dynamic(_) => return Type::from(superclass).member(db, name),
|
||||
ClassBase::Class(class) => {
|
||||
let member = class.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
}
|
||||
ClassBase::Dynamic(_) => {
|
||||
// Note: calling `Type::from(superclass).member()` would be incorrect here.
|
||||
// What we'd really want is a `Type::Any.own_class_member()` method,
|
||||
// but adding such a method wouldn't make much sense -- it would always return `Any`!
|
||||
dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass));
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
lookup_result = lookup_result.or_else(|lookup_error| {
|
||||
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
|
||||
});
|
||||
}
|
||||
}
|
||||
if lookup_result.is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Unbound
|
||||
match (Symbol::from(lookup_result), dynamic_type_to_intersect_with) {
|
||||
(symbol, None) => symbol,
|
||||
(Symbol::Type(ty, _), Some(dynamic_type)) => Symbol::bound(
|
||||
IntersectionBuilder::new(db)
|
||||
.add_positive(ty)
|
||||
.add_positive(dynamic_type)
|
||||
.build(),
|
||||
),
|
||||
(Symbol::Unbound, Some(dynamic_type)) => Symbol::bound(dynamic_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
@@ -4293,7 +4442,7 @@ impl<'db> Class<'db> {
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
match symbol_from_declarations(db, declarations, RequiresExplicitReExport::No) {
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
// The attribute is declared in the class body.
|
||||
|
||||
@@ -4315,7 +4464,7 @@ impl<'db> Class<'db> {
|
||||
// in a method, and it could also be *bound* in the class body (and/or in a method).
|
||||
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
let inferred = symbol_from_bindings(db, bindings, RequiresExplicitReExport::No);
|
||||
let inferred_ty = inferred.ignore_possibly_unbound();
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
|
||||
@@ -4857,12 +5006,12 @@ pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::{setup_db, TestDbBuilder};
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::PythonVersion;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::types::diagnostic::{
|
||||
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::signatures::Parameter;
|
||||
use crate::types::UnionType;
|
||||
use crate::types::{todo_type, UnionType};
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
/// Bind a [`CallArguments`] against a callable [`Signature`].
|
||||
@@ -16,7 +16,7 @@ pub(crate) fn bind_call<'db>(
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
signature: &Signature<'db>,
|
||||
callable_ty: Option<Type<'db>>,
|
||||
callable_ty: Type<'db>,
|
||||
) -> CallBinding<'db> {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
@@ -138,7 +138,7 @@ pub(crate) fn bind_call<'db>(
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CallBinding<'db> {
|
||||
/// Type of the callable object (function, class...)
|
||||
callable_ty: Option<Type<'db>>,
|
||||
callable_ty: Type<'db>,
|
||||
|
||||
/// Return type of the call.
|
||||
return_ty: Type<'db>,
|
||||
@@ -154,7 +154,7 @@ impl<'db> CallBinding<'db> {
|
||||
// TODO remove this constructor and construct always from `bind_call`
|
||||
pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_ty: None,
|
||||
callable_ty: todo_type!("CallBinding::from_return_type"),
|
||||
return_ty,
|
||||
parameter_tys: Box::default(),
|
||||
errors: vec![],
|
||||
@@ -189,8 +189,8 @@ impl<'db> CallBinding<'db> {
|
||||
|
||||
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
|
||||
match self.callable_ty {
|
||||
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
|
||||
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
|
||||
Type::FunctionLiteral(function) => Some(function.name(db)),
|
||||
Type::ClassLiteral(class_type) => Some(class_type.class.name(db)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,41 +826,77 @@ impl Ranged for TypeCheckDiagnostic {
|
||||
/// each Salsa-struct comes with an overhead.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
inner: Option<Box<TypeCheckDiagnosticsInner>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
struct TypeCheckDiagnosticsInner {
|
||||
diagnostics: Vec<Arc<TypeCheckDiagnostic>>,
|
||||
used_suppressions: FxHashSet<FileSuppressionId>,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostics {
|
||||
pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
|
||||
self.diagnostics.push(Arc::new(diagnostic));
|
||||
let inner = self.get_mut_inner();
|
||||
inner.diagnostics.push(Arc::new(diagnostic));
|
||||
}
|
||||
|
||||
pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) {
|
||||
self.diagnostics.extend_from_slice(&other.diagnostics);
|
||||
self.used_suppressions.extend(&other.used_suppressions);
|
||||
let Some(other_inner) = other.inner.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let inner = self.get_mut_inner();
|
||||
inner
|
||||
.diagnostics
|
||||
.extend_from_slice(&other_inner.diagnostics);
|
||||
inner
|
||||
.used_suppressions
|
||||
.extend(&other_inner.used_suppressions);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) {
|
||||
self.used_suppressions.insert(suppression_id);
|
||||
let inner = self.get_mut_inner();
|
||||
inner.used_suppressions.insert(suppression_id);
|
||||
}
|
||||
|
||||
pub(crate) fn diagnostics(&self) -> &[Arc<TypeCheckDiagnostic>] {
|
||||
self.get_inner()
|
||||
.map(|inner| &*inner.diagnostics)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool {
|
||||
self.used_suppressions.contains(&suppression_id)
|
||||
self.get_inner()
|
||||
.is_some_and(|inner| inner.used_suppressions.contains(&suppression_id))
|
||||
}
|
||||
|
||||
pub(crate) fn used_len(&self) -> usize {
|
||||
self.used_suppressions.len()
|
||||
self.get_inner()
|
||||
.map(|inner| inner.used_suppressions.len())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn shrink_to_fit(&mut self) {
|
||||
self.used_suppressions.shrink_to_fit();
|
||||
self.diagnostics.shrink_to_fit();
|
||||
if let Some(inner) = self.inner.as_mut() {
|
||||
inner.used_suppressions.shrink_to_fit();
|
||||
inner.diagnostics.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mut_inner(&mut self) -> &mut TypeCheckDiagnosticsInner {
|
||||
self.inner
|
||||
.get_or_insert_with(|| Box::new(TypeCheckDiagnosticsInner::default()))
|
||||
}
|
||||
|
||||
fn get_inner(&self) -> Option<&TypeCheckDiagnosticsInner> {
|
||||
self.inner.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TypeCheckDiagnostics {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.diagnostics.fmt(f)
|
||||
self.diagnostics().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,7 +904,7 @@ impl Deref for TypeCheckDiagnostics {
|
||||
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.diagnostics
|
||||
self.diagnostics()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,7 +913,10 @@ impl IntoIterator for TypeCheckDiagnostics {
|
||||
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.diagnostics.into_iter()
|
||||
self.inner
|
||||
.map(|inner| inner.diagnostics)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,7 +925,7 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
|
||||
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.diagnostics.iter()
|
||||
self.diagnostics().iter()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::stdlib::builtins_module_scope;
|
||||
use crate::symbol::LookupError;
|
||||
use crate::types::call::{Argument, CallArguments};
|
||||
use crate::types::diagnostic::{
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||
@@ -63,13 +64,13 @@ use crate::types::diagnostic::{
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
todo_type, typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType,
|
||||
DynamicType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType,
|
||||
IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate,
|
||||
MetaclassErrorKind, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness,
|
||||
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
builtins_symbol, symbol, symbol_from_bindings, symbol_from_declarations, todo_type,
|
||||
typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType,
|
||||
FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome,
|
||||
KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind,
|
||||
RequiresExplicitReExport, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers,
|
||||
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
|
||||
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
@@ -86,7 +87,7 @@ use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use super::{ParameterExpectation, ParameterExpectations};
|
||||
use super::{global_symbol, ParameterExpectation, ParameterExpectations};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
@@ -735,7 +736,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_type_alias_definition(type_alias.node(), definition);
|
||||
}
|
||||
DefinitionKind::Import(import) => {
|
||||
self.infer_import_definition(import.node(), definition);
|
||||
self.infer_import_definition(import.alias(), definition);
|
||||
}
|
||||
DefinitionKind::ImportFrom(import_from) => {
|
||||
self.infer_import_from_definition(
|
||||
@@ -871,22 +872,25 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let use_def = self.index.use_def_map(binding.file_scope(self.db()));
|
||||
let declarations = use_def.declarations_at_binding(binding);
|
||||
let mut bound_ty = ty;
|
||||
let declared_ty = symbol_from_declarations(self.db(), declarations)
|
||||
.map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
|
||||
.unwrap_or_else(|(ty, conflicting)| {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
|
||||
let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name();
|
||||
self.context.report_lint(
|
||||
&CONFLICTING_DECLARATIONS,
|
||||
node,
|
||||
format_args!(
|
||||
"Conflicting declared types for `{symbol_name}`: {}",
|
||||
conflicting.display(self.db())
|
||||
),
|
||||
);
|
||||
ty.inner_type()
|
||||
});
|
||||
let declared_ty =
|
||||
symbol_from_declarations(self.db(), declarations, RequiresExplicitReExport::No)
|
||||
.map(|SymbolAndQualifiers(s, _)| {
|
||||
s.ignore_possibly_unbound().unwrap_or(Type::unknown())
|
||||
})
|
||||
.unwrap_or_else(|(ty, conflicting)| {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
|
||||
let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name();
|
||||
self.context.report_lint(
|
||||
&CONFLICTING_DECLARATIONS,
|
||||
node,
|
||||
format_args!(
|
||||
"Conflicting declared types for `{symbol_name}`: {}",
|
||||
conflicting.display(self.db())
|
||||
),
|
||||
);
|
||||
ty.inner_type()
|
||||
});
|
||||
if !bound_ty.is_assignable_to(self.db(), declared_ty) {
|
||||
report_invalid_assignment(&self.context, node, declared_ty, bound_ty);
|
||||
// allow declarations to override inference in case of invalid assignment
|
||||
@@ -906,9 +910,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
|
||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = symbol_from_bindings(self.db(), prior_bindings)
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Never);
|
||||
let inferred_ty =
|
||||
symbol_from_bindings(self.db(), prior_bindings, RequiresExplicitReExport::No)
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Never);
|
||||
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) {
|
||||
ty
|
||||
} else {
|
||||
@@ -3307,7 +3312,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// If we're inferring types of deferred expressions, always treat them as public symbols
|
||||
let local_scope_symbol = if self.is_deferred() {
|
||||
if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) {
|
||||
symbol_from_bindings(db, use_def.public_bindings(symbol_id))
|
||||
symbol_from_bindings(
|
||||
db,
|
||||
use_def.public_bindings(symbol_id),
|
||||
RequiresExplicitReExport::No,
|
||||
)
|
||||
} else {
|
||||
assert!(
|
||||
self.deferred_state.in_string_annotation(),
|
||||
@@ -3317,7 +3326,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
let use_id = name_node.scoped_use_id(db, scope);
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
symbol_from_bindings(
|
||||
db,
|
||||
use_def.bindings_at_use(use_id),
|
||||
RequiresExplicitReExport::No,
|
||||
)
|
||||
};
|
||||
|
||||
let symbol = local_scope_symbol.or_fall_back_to(db, || {
|
||||
@@ -3409,17 +3422,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
})
|
||||
});
|
||||
|
||||
match symbol {
|
||||
Symbol::Type(ty, Boundness::Bound) => ty,
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name_node);
|
||||
ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
symbol.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound => {
|
||||
report_unresolved_reference(&self.context, name_node);
|
||||
Type::unknown()
|
||||
}
|
||||
}
|
||||
LookupError::PossiblyUnbound(type_when_bound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name_node);
|
||||
type_when_bound
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
|
||||
@@ -3439,34 +3451,37 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
ctx: _,
|
||||
} = attribute;
|
||||
|
||||
let value_ty = self.infer_expression(value);
|
||||
match value_ty.member(self.db(), &attr.id) {
|
||||
Symbol::Type(member_ty, Boundness::Bound) => member_ty,
|
||||
Symbol::Type(member_ty, Boundness::PossiblyUnbound) => {
|
||||
self.context.report_lint(
|
||||
&POSSIBLY_UNBOUND_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Attribute `{}` on type `{}` is possibly unbound",
|
||||
attr.id,
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
member_ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Type `{}` has no attribute `{}`",
|
||||
value_ty.display(self.db()),
|
||||
attr.id
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
}
|
||||
let value_type = self.infer_expression(value);
|
||||
let db = self.db();
|
||||
|
||||
value_type
|
||||
.member(db, &attr.id)
|
||||
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound => {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Type `{}` has no attribute `{}`",
|
||||
value_type.display(db),
|
||||
attr.id
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
LookupError::PossiblyUnbound(type_when_bound) => {
|
||||
self.context.report_lint(
|
||||
&POSSIBLY_UNBOUND_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Attribute `{}` on type `{}` is possibly unbound",
|
||||
attr.id,
|
||||
value_type.display(db),
|
||||
),
|
||||
);
|
||||
type_when_bound
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||
@@ -3824,6 +3839,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
|
||||
let reflected_dunder = op.reflected_dunder();
|
||||
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
|
||||
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
|
||||
// CallOutcomes together
|
||||
if !rhs_reflected.is_unbound()
|
||||
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
|
||||
{
|
||||
@@ -6076,7 +6093,7 @@ mod tests {
|
||||
let mut db = setup_db();
|
||||
let content = format!(
|
||||
r#"
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import Literal, assert_type
|
||||
|
||||
assert_type(not "{y}", bool)
|
||||
assert_type(not 10*"{y}", bool)
|
||||
@@ -6098,7 +6115,7 @@ mod tests {
|
||||
let mut db = setup_db();
|
||||
let content = format!(
|
||||
r#"
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import Literal, LiteralString, assert_type
|
||||
|
||||
assert_type(2 * "hello", Literal["hellohello"])
|
||||
assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"])
|
||||
@@ -6123,7 +6140,7 @@ mod tests {
|
||||
let mut db = setup_db();
|
||||
let content = format!(
|
||||
r#"
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import Literal, LiteralString, assert_type
|
||||
|
||||
assert_type("{y}", LiteralString)
|
||||
assert_type(10*"{y}", LiteralString)
|
||||
@@ -6145,7 +6162,7 @@ mod tests {
|
||||
let mut db = setup_db();
|
||||
let content = format!(
|
||||
r#"
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import LiteralString, assert_type
|
||||
|
||||
assert_type("{y}", LiteralString)
|
||||
assert_type("a" + "{z}", LiteralString)
|
||||
@@ -6165,7 +6182,7 @@ mod tests {
|
||||
let mut db = setup_db();
|
||||
let content = format!(
|
||||
r#"
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import LiteralString, assert_type
|
||||
|
||||
assert_type("{y}", LiteralString)
|
||||
assert_type("{y}" + "a", LiteralString)
|
||||
|
||||
@@ -357,6 +357,8 @@ mod tests {
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
from typing import Literal
|
||||
|
||||
def f(a, b: int, c = 1, d: int = 2, /,
|
||||
e = 3, f: Literal[4] = 4, *args: object,
|
||||
g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ...
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! ```
|
||||
|
||||
use anyhow::Context;
|
||||
use red_knot_python_semantic::{PythonPlatform, PythonVersion};
|
||||
use red_knot_python_semantic::PythonPlatform;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::sync::Arc;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{
|
||||
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,
|
||||
PythonVersion, SearchPathSettings,
|
||||
SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -1 +1 @@
|
||||
c193cd2a36839c8e6336f350397f51ce52fedd5e
|
||||
cc8ca939c0477a49fcce0554fa1743bd5c656a11
|
||||
|
||||
@@ -78,7 +78,7 @@ if sys.platform == "win32":
|
||||
SO_EXCLUSIVEADDRUSE: int
|
||||
if sys.platform != "win32":
|
||||
SO_REUSEPORT: int
|
||||
if sys.platform != "darwin" or sys.version_info >= (3, 13):
|
||||
if sys.platform != "darwin":
|
||||
SO_BINDTODEVICE: int
|
||||
|
||||
if sys.platform != "win32" and sys.platform != "darwin":
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
from _typeshed import SupportsWrite, sentinel
|
||||
from collections.abc import Callable, Generator, Iterable, Sequence
|
||||
from re import Pattern
|
||||
from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeVar, overload
|
||||
from typing import IO, Any, ClassVar, Final, Generic, NoReturn, Protocol, TypeVar, overload
|
||||
from typing_extensions import Self, TypeAlias, deprecated
|
||||
|
||||
__all__ = [
|
||||
@@ -33,25 +33,14 @@ _ActionT = TypeVar("_ActionT", bound=Action)
|
||||
_ArgumentParserT = TypeVar("_ArgumentParserT", bound=ArgumentParser)
|
||||
_N = TypeVar("_N")
|
||||
_ActionType: TypeAlias = Callable[[str], Any] | FileType | str
|
||||
# more precisely, Literal["store", "store_const", "store_true",
|
||||
# "store_false", "append", "append_const", "count", "help", "version",
|
||||
# "extend"], but using this would make it hard to annotate callers
|
||||
# that don't use a literal argument
|
||||
_ActionStr: TypeAlias = str
|
||||
# more precisely, Literal["?", "*", "+", "...", "A...",
|
||||
# "==SUPPRESS=="], but using this would make it hard to annotate
|
||||
# callers that don't use a literal argument
|
||||
_NArgsStr: TypeAlias = str
|
||||
|
||||
ONE_OR_MORE: Final = "+"
|
||||
OPTIONAL: Final = "?"
|
||||
PARSER: Final = "A..."
|
||||
REMAINDER: Final = "..."
|
||||
_SUPPRESS_T = NewType("_SUPPRESS_T", str)
|
||||
SUPPRESS: _SUPPRESS_T | str # not using Literal because argparse sometimes compares SUPPRESS with is
|
||||
# the | str is there so that foo = argparse.SUPPRESS; foo = "test" checks out in mypy
|
||||
SUPPRESS: Final = "==SUPPRESS=="
|
||||
ZERO_OR_MORE: Final = "*"
|
||||
_UNRECOGNIZED_ARGS_ATTR: Final[str] # undocumented
|
||||
_UNRECOGNIZED_ARGS_ATTR: Final = "_unrecognized_args" # undocumented
|
||||
|
||||
class ArgumentError(Exception):
|
||||
argument_name: str | None
|
||||
@@ -86,8 +75,13 @@ class _ActionsContainer:
|
||||
def add_argument(
|
||||
self,
|
||||
*name_or_flags: str,
|
||||
action: _ActionStr | type[Action] = ...,
|
||||
nargs: int | _NArgsStr | _SUPPRESS_T | None = None,
|
||||
# str covers predefined actions ("store_true", "count", etc.)
|
||||
# and user registered actions via the `register` method.
|
||||
action: str | type[Action] = ...,
|
||||
# more precisely, Literal["?", "*", "+", "...", "A...", "==SUPPRESS=="],
|
||||
# but using this would make it hard to annotate callers that don't use a
|
||||
# literal argument and for subclasses to override this method.
|
||||
nargs: int | str | None = None,
|
||||
const: Any = ...,
|
||||
default: Any = ...,
|
||||
type: _ActionType = ...,
|
||||
|
||||
@@ -79,6 +79,7 @@ if sys.version_info >= (3, 12):
|
||||
_FutureLike: TypeAlias = Future[_T] | Awaitable[_T]
|
||||
else:
|
||||
_FutureLike: TypeAlias = Future[_T] | Generator[Any, None, _T] | Awaitable[_T]
|
||||
|
||||
_TaskYieldType: TypeAlias = Future[object] | None
|
||||
|
||||
FIRST_COMPLETED = concurrent.futures.FIRST_COMPLETED
|
||||
@@ -347,7 +348,8 @@ else:
|
||||
*coros_or_futures: _FutureLike[_T], loop: AbstractEventLoop | None = None, return_exceptions: bool
|
||||
) -> Future[list[_T | BaseException]]: ...
|
||||
|
||||
def run_coroutine_threadsafe(coro: _FutureLike[_T], loop: AbstractEventLoop) -> concurrent.futures.Future[_T]: ...
|
||||
# unlike some asyncio apis, This does strict runtime checking of actually being a coroutine, not of any future-like.
|
||||
def run_coroutine_threadsafe(coro: Coroutine[Any, Any, _T], loop: AbstractEventLoop) -> concurrent.futures.Future[_T]: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
def shield(arg: _FutureLike[_T]) -> Future[_T]: ...
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
from _typeshed import ExcInfo, TraceFunction, Unused
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from types import CodeType, FrameType, TracebackType
|
||||
from typing import IO, Any, Final, SupportsInt, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
@@ -30,6 +31,10 @@ class Bdb:
|
||||
def __init__(self, skip: Iterable[str] | None = None) -> None: ...
|
||||
def canonic(self, filename: str) -> str: ...
|
||||
def reset(self) -> None: ...
|
||||
if sys.version_info >= (3, 12):
|
||||
@contextmanager
|
||||
def set_enterframe(self, frame: FrameType) -> Iterator[None]: ...
|
||||
|
||||
def trace_dispatch(self, frame: FrameType, event: str, arg: Any) -> TraceFunction: ...
|
||||
def dispatch_line(self, frame: FrameType) -> TraceFunction: ...
|
||||
def dispatch_call(self, frame: FrameType, arg: None) -> TraceFunction: ...
|
||||
@@ -73,7 +78,7 @@ class Bdb:
|
||||
def get_file_breaks(self, filename: str) -> list[Breakpoint]: ...
|
||||
def get_all_breaks(self) -> list[Breakpoint]: ...
|
||||
def get_stack(self, f: FrameType | None, t: TracebackType | None) -> tuple[list[tuple[FrameType, int]], int]: ...
|
||||
def format_stack_entry(self, frame_lineno: int, lprefix: str = ": ") -> str: ...
|
||||
def format_stack_entry(self, frame_lineno: tuple[FrameType, int], lprefix: str = ": ") -> str: ...
|
||||
def run(
|
||||
self, cmd: str | CodeType, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None
|
||||
) -> None: ...
|
||||
|
||||
@@ -1295,7 +1295,7 @@ def ascii(obj: object, /) -> str: ...
|
||||
def bin(number: int | SupportsIndex, /) -> str: ...
|
||||
def breakpoint(*args: Any, **kws: Any) -> None: ...
|
||||
def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ...
|
||||
def chr(i: int, /) -> str: ...
|
||||
def chr(i: int | SupportsIndex, /) -> str: ...
|
||||
|
||||
# We define this here instead of using os.PathLike to avoid import cycle issues.
|
||||
# See https://github.com/python/typeshed/pull/991#issuecomment-288160993
|
||||
|
||||
@@ -32,9 +32,9 @@ _T = TypeVar("_T")
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_T_io = TypeVar("_T_io", bound=IO[str] | None)
|
||||
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None)
|
||||
_F = TypeVar("_F", bound=Callable[..., Any])
|
||||
_G = TypeVar("_G", bound=Generator[Any, Any, Any] | AsyncGenerator[Any, Any], covariant=True)
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
_SendT_contra = TypeVar("_SendT_contra", contravariant=True, default=None)
|
||||
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True, default=None)
|
||||
@@ -64,13 +64,9 @@ class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ign
|
||||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||
) -> _ExitT_co: ...
|
||||
|
||||
class _WrappedCallable(Generic[_P, _R]):
|
||||
__wrapped__: Callable[_P, _R]
|
||||
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
||||
|
||||
class ContextDecorator:
|
||||
def _recreate_cm(self) -> Self: ...
|
||||
def __call__(self, func: Callable[_P, _R]) -> _WrappedCallable[_P, _R]: ...
|
||||
def __call__(self, func: _F) -> _F: ...
|
||||
|
||||
class _GeneratorContextManagerBase(Generic[_G]):
|
||||
# Ideally this would use ParamSpec, but that requires (*args, **kwargs), which this isn't. see #6676
|
||||
@@ -97,11 +93,11 @@ class _GeneratorContextManager(
|
||||
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
_AR = TypeVar("_AR", bound=Awaitable[Any])
|
||||
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
|
||||
|
||||
class AsyncContextDecorator:
|
||||
def _recreate_cm(self) -> Self: ...
|
||||
def __call__(self, func: Callable[_P, _AR]) -> _WrappedCallable[_P, _AR]: ...
|
||||
def __call__(self, func: _AF) -> _AF: ...
|
||||
|
||||
class _AsyncGeneratorContextManager(
|
||||
_GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator
|
||||
from email.errors import HeaderParseError, MessageDefect
|
||||
from email.policy import Policy
|
||||
@@ -21,6 +22,9 @@ NLSET: Final[set[str]]
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
SPECIALSNL: Final[set[str]]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def make_quoted_pairs(value: Any) -> str: ...
|
||||
|
||||
def quote_string(value: Any) -> str: ...
|
||||
|
||||
rfc2047_matcher: Pattern[str]
|
||||
|
||||
@@ -64,7 +64,11 @@ if sys.version_info >= (3, 11):
|
||||
def __init__(self, value: _EnumMemberT) -> None: ...
|
||||
|
||||
class _EnumDict(dict[str, Any]):
|
||||
def __init__(self) -> None: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def __init__(self, cls_name: str | None = None) -> None: ...
|
||||
else:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None: ...
|
||||
if sys.version_info >= (3, 11):
|
||||
# See comment above `typing.MutableMapping.update`
|
||||
|
||||
@@ -61,7 +61,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
client_address: _socket._RetAddress,
|
||||
server: socketserver.BaseServer,
|
||||
*,
|
||||
directory: str | None = None,
|
||||
directory: StrPath | None = None,
|
||||
) -> None: ...
|
||||
def do_GET(self) -> None: ...
|
||||
def do_HEAD(self) -> None: ...
|
||||
|
||||
@@ -2,7 +2,7 @@ import builtins
|
||||
from _typeshed import MaybeNone, SupportsWrite
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from typing import Any, ClassVar, Literal, NoReturn, overload
|
||||
from typing import Any, ClassVar, Final, Literal, NoReturn, overload
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = [
|
||||
@@ -24,10 +24,10 @@ __all__ = [
|
||||
"BadOptionError",
|
||||
"check_choice",
|
||||
]
|
||||
|
||||
NO_DEFAULT: tuple[str, ...]
|
||||
SUPPRESS_HELP: str
|
||||
SUPPRESS_USAGE: str
|
||||
# pytype is not happy with `NO_DEFAULT: Final = ("NO", "DEFAULT")`
|
||||
NO_DEFAULT: Final[tuple[Literal["NO"], Literal["DEFAULT"]]]
|
||||
SUPPRESS_HELP: Final = "SUPPRESSHELP"
|
||||
SUPPRESS_USAGE: Final = "SUPPRESSUSAGE"
|
||||
|
||||
# Can return complex, float, or int depending on the option's type
|
||||
def check_builtin(option: Option, opt: str, value: str) -> complex: ...
|
||||
|
||||
@@ -240,6 +240,7 @@ if sys.platform == "linux" and sys.version_info >= (3, 12):
|
||||
"CLONE_VM",
|
||||
"setns",
|
||||
"unshare",
|
||||
"PIDFD_NONBLOCK",
|
||||
]
|
||||
if sys.platform == "linux" and sys.version_info >= (3, 10):
|
||||
__all__ += [
|
||||
@@ -1603,6 +1604,9 @@ if sys.version_info >= (3, 9):
|
||||
if sys.platform == "linux":
|
||||
def pidfd_open(pid: int, flags: int = ...) -> int: ...
|
||||
|
||||
if sys.version_info >= (3, 12) and sys.platform == "linux":
|
||||
PIDFD_NONBLOCK: Final = 2048
|
||||
|
||||
if sys.version_info >= (3, 12) and sys.platform == "win32":
|
||||
def listdrives() -> list[str]: ...
|
||||
def listmounts(volume: str) -> list[str]: ...
|
||||
|
||||
@@ -379,6 +379,7 @@ if sys.platform != "win32":
|
||||
CLONE_SYSVSEM as CLONE_SYSVSEM,
|
||||
CLONE_THREAD as CLONE_THREAD,
|
||||
CLONE_VM as CLONE_VM,
|
||||
PIDFD_NONBLOCK as PIDFD_NONBLOCK,
|
||||
setns as setns,
|
||||
unshare as unshare,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import sre_constants
|
||||
import sys
|
||||
from _typeshed import MaybeNone, ReadableBuffer
|
||||
from collections.abc import Callable, Iterator, Mapping
|
||||
from typing import Any, AnyStr, Generic, Literal, TypeVar, final, overload
|
||||
from typing import Any, AnyStr, Final, Generic, Literal, TypeVar, final, overload
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
@@ -224,25 +224,27 @@ class RegexFlag(enum.IntFlag):
|
||||
if sys.version_info >= (3, 11):
|
||||
NOFLAG = 0
|
||||
|
||||
A = RegexFlag.A
|
||||
ASCII = RegexFlag.ASCII
|
||||
DEBUG = RegexFlag.DEBUG
|
||||
I = RegexFlag.I
|
||||
IGNORECASE = RegexFlag.IGNORECASE
|
||||
L = RegexFlag.L
|
||||
LOCALE = RegexFlag.LOCALE
|
||||
M = RegexFlag.M
|
||||
MULTILINE = RegexFlag.MULTILINE
|
||||
S = RegexFlag.S
|
||||
DOTALL = RegexFlag.DOTALL
|
||||
X = RegexFlag.X
|
||||
VERBOSE = RegexFlag.VERBOSE
|
||||
U = RegexFlag.U
|
||||
UNICODE = RegexFlag.UNICODE
|
||||
A: Final = RegexFlag.A
|
||||
ASCII: Final = RegexFlag.ASCII
|
||||
DEBUG: Final = RegexFlag.DEBUG
|
||||
I: Final = RegexFlag.I
|
||||
IGNORECASE: Final = RegexFlag.IGNORECASE
|
||||
L: Final = RegexFlag.L
|
||||
LOCALE: Final = RegexFlag.LOCALE
|
||||
M: Final = RegexFlag.M
|
||||
MULTILINE: Final = RegexFlag.MULTILINE
|
||||
S: Final = RegexFlag.S
|
||||
DOTALL: Final = RegexFlag.DOTALL
|
||||
X: Final = RegexFlag.X
|
||||
VERBOSE: Final = RegexFlag.VERBOSE
|
||||
U: Final = RegexFlag.U
|
||||
UNICODE: Final = RegexFlag.UNICODE
|
||||
if sys.version_info < (3, 13):
|
||||
T = RegexFlag.T
|
||||
TEMPLATE = RegexFlag.TEMPLATE
|
||||
T: Final = RegexFlag.T
|
||||
TEMPLATE: Final = RegexFlag.TEMPLATE
|
||||
if sys.version_info >= (3, 11):
|
||||
# pytype chokes on `NOFLAG: Final = RegexFlag.NOFLAG` with `LiteralValueError`
|
||||
# mypy chokes on `NOFLAG: Final[Literal[RegexFlag.NOFLAG]]` with `Literal[...] is invalid`
|
||||
NOFLAG = RegexFlag.NOFLAG
|
||||
_FlagsType: TypeAlias = int | RegexFlag
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class _RmtreeType(Protocol):
|
||||
self,
|
||||
path: StrOrBytesPath,
|
||||
ignore_errors: bool,
|
||||
onerror: _OnErrorCallback,
|
||||
onerror: _OnErrorCallback | None,
|
||||
*,
|
||||
onexc: None = None,
|
||||
dir_fd: int | None = None,
|
||||
@@ -95,7 +95,7 @@ class _RmtreeType(Protocol):
|
||||
path: StrOrBytesPath,
|
||||
ignore_errors: bool = False,
|
||||
*,
|
||||
onerror: _OnErrorCallback,
|
||||
onerror: _OnErrorCallback | None,
|
||||
onexc: None = None,
|
||||
dir_fd: int | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@@ -515,7 +515,7 @@ if sys.platform != "win32":
|
||||
"IPV6_RTHDRDSTOPTS",
|
||||
]
|
||||
|
||||
if sys.platform != "darwin" or sys.version_info >= (3, 13):
|
||||
if sys.platform != "darwin":
|
||||
from _socket import SO_BINDTODEVICE as SO_BINDTODEVICE
|
||||
|
||||
__all__ += ["SO_BINDTODEVICE"]
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import sys
|
||||
from re import error as error
|
||||
from typing import Any
|
||||
from typing import Final
|
||||
from typing_extensions import Self
|
||||
|
||||
MAXGROUPS: int
|
||||
MAXGROUPS: Final[int]
|
||||
|
||||
MAGIC: int
|
||||
MAGIC: Final[int]
|
||||
|
||||
class _NamedIntConstant(int):
|
||||
name: Any
|
||||
name: str
|
||||
def __new__(cls, value: int, name: str) -> Self: ...
|
||||
|
||||
MAXREPEAT: _NamedIntConstant
|
||||
MAXREPEAT: Final[_NamedIntConstant]
|
||||
OPCODES: list[_NamedIntConstant]
|
||||
ATCODES: list[_NamedIntConstant]
|
||||
CHCODES: list[_NamedIntConstant]
|
||||
@@ -23,102 +23,104 @@ AT_LOCALE: dict[_NamedIntConstant, _NamedIntConstant]
|
||||
AT_UNICODE: dict[_NamedIntConstant, _NamedIntConstant]
|
||||
CH_LOCALE: dict[_NamedIntConstant, _NamedIntConstant]
|
||||
CH_UNICODE: dict[_NamedIntConstant, _NamedIntConstant]
|
||||
# flags
|
||||
if sys.version_info < (3, 13):
|
||||
SRE_FLAG_TEMPLATE: int
|
||||
SRE_FLAG_IGNORECASE: int
|
||||
SRE_FLAG_LOCALE: int
|
||||
SRE_FLAG_MULTILINE: int
|
||||
SRE_FLAG_DOTALL: int
|
||||
SRE_FLAG_UNICODE: int
|
||||
SRE_FLAG_VERBOSE: int
|
||||
SRE_FLAG_DEBUG: int
|
||||
SRE_FLAG_ASCII: int
|
||||
SRE_INFO_PREFIX: int
|
||||
SRE_INFO_LITERAL: int
|
||||
SRE_INFO_CHARSET: int
|
||||
SRE_FLAG_TEMPLATE: Final = 1
|
||||
SRE_FLAG_IGNORECASE: Final = 2
|
||||
SRE_FLAG_LOCALE: Final = 4
|
||||
SRE_FLAG_MULTILINE: Final = 8
|
||||
SRE_FLAG_DOTALL: Final = 16
|
||||
SRE_FLAG_UNICODE: Final = 32
|
||||
SRE_FLAG_VERBOSE: Final = 64
|
||||
SRE_FLAG_DEBUG: Final = 128
|
||||
SRE_FLAG_ASCII: Final = 256
|
||||
# flags for INFO primitive
|
||||
SRE_INFO_PREFIX: Final = 1
|
||||
SRE_INFO_LITERAL: Final = 2
|
||||
SRE_INFO_CHARSET: Final = 4
|
||||
|
||||
# Stubgen above; manually defined constants below (dynamic at runtime)
|
||||
|
||||
# from OPCODES
|
||||
FAILURE: _NamedIntConstant
|
||||
SUCCESS: _NamedIntConstant
|
||||
ANY: _NamedIntConstant
|
||||
ANY_ALL: _NamedIntConstant
|
||||
ASSERT: _NamedIntConstant
|
||||
ASSERT_NOT: _NamedIntConstant
|
||||
AT: _NamedIntConstant
|
||||
BRANCH: _NamedIntConstant
|
||||
FAILURE: Final[_NamedIntConstant]
|
||||
SUCCESS: Final[_NamedIntConstant]
|
||||
ANY: Final[_NamedIntConstant]
|
||||
ANY_ALL: Final[_NamedIntConstant]
|
||||
ASSERT: Final[_NamedIntConstant]
|
||||
ASSERT_NOT: Final[_NamedIntConstant]
|
||||
AT: Final[_NamedIntConstant]
|
||||
BRANCH: Final[_NamedIntConstant]
|
||||
if sys.version_info < (3, 11):
|
||||
CALL: _NamedIntConstant
|
||||
CATEGORY: _NamedIntConstant
|
||||
CHARSET: _NamedIntConstant
|
||||
BIGCHARSET: _NamedIntConstant
|
||||
GROUPREF: _NamedIntConstant
|
||||
GROUPREF_EXISTS: _NamedIntConstant
|
||||
GROUPREF_IGNORE: _NamedIntConstant
|
||||
IN: _NamedIntConstant
|
||||
IN_IGNORE: _NamedIntConstant
|
||||
INFO: _NamedIntConstant
|
||||
JUMP: _NamedIntConstant
|
||||
LITERAL: _NamedIntConstant
|
||||
LITERAL_IGNORE: _NamedIntConstant
|
||||
MARK: _NamedIntConstant
|
||||
MAX_UNTIL: _NamedIntConstant
|
||||
MIN_UNTIL: _NamedIntConstant
|
||||
NOT_LITERAL: _NamedIntConstant
|
||||
NOT_LITERAL_IGNORE: _NamedIntConstant
|
||||
NEGATE: _NamedIntConstant
|
||||
RANGE: _NamedIntConstant
|
||||
REPEAT: _NamedIntConstant
|
||||
REPEAT_ONE: _NamedIntConstant
|
||||
SUBPATTERN: _NamedIntConstant
|
||||
MIN_REPEAT_ONE: _NamedIntConstant
|
||||
CALL: Final[_NamedIntConstant]
|
||||
CATEGORY: Final[_NamedIntConstant]
|
||||
CHARSET: Final[_NamedIntConstant]
|
||||
BIGCHARSET: Final[_NamedIntConstant]
|
||||
GROUPREF: Final[_NamedIntConstant]
|
||||
GROUPREF_EXISTS: Final[_NamedIntConstant]
|
||||
GROUPREF_IGNORE: Final[_NamedIntConstant]
|
||||
IN: Final[_NamedIntConstant]
|
||||
IN_IGNORE: Final[_NamedIntConstant]
|
||||
INFO: Final[_NamedIntConstant]
|
||||
JUMP: Final[_NamedIntConstant]
|
||||
LITERAL: Final[_NamedIntConstant]
|
||||
LITERAL_IGNORE: Final[_NamedIntConstant]
|
||||
MARK: Final[_NamedIntConstant]
|
||||
MAX_UNTIL: Final[_NamedIntConstant]
|
||||
MIN_UNTIL: Final[_NamedIntConstant]
|
||||
NOT_LITERAL: Final[_NamedIntConstant]
|
||||
NOT_LITERAL_IGNORE: Final[_NamedIntConstant]
|
||||
NEGATE: Final[_NamedIntConstant]
|
||||
RANGE: Final[_NamedIntConstant]
|
||||
REPEAT: Final[_NamedIntConstant]
|
||||
REPEAT_ONE: Final[_NamedIntConstant]
|
||||
SUBPATTERN: Final[_NamedIntConstant]
|
||||
MIN_REPEAT_ONE: Final[_NamedIntConstant]
|
||||
if sys.version_info >= (3, 11):
|
||||
ATOMIC_GROUP: _NamedIntConstant
|
||||
POSSESSIVE_REPEAT: _NamedIntConstant
|
||||
POSSESSIVE_REPEAT_ONE: _NamedIntConstant
|
||||
RANGE_UNI_IGNORE: _NamedIntConstant
|
||||
GROUPREF_LOC_IGNORE: _NamedIntConstant
|
||||
GROUPREF_UNI_IGNORE: _NamedIntConstant
|
||||
IN_LOC_IGNORE: _NamedIntConstant
|
||||
IN_UNI_IGNORE: _NamedIntConstant
|
||||
LITERAL_LOC_IGNORE: _NamedIntConstant
|
||||
LITERAL_UNI_IGNORE: _NamedIntConstant
|
||||
NOT_LITERAL_LOC_IGNORE: _NamedIntConstant
|
||||
NOT_LITERAL_UNI_IGNORE: _NamedIntConstant
|
||||
MIN_REPEAT: _NamedIntConstant
|
||||
MAX_REPEAT: _NamedIntConstant
|
||||
ATOMIC_GROUP: Final[_NamedIntConstant]
|
||||
POSSESSIVE_REPEAT: Final[_NamedIntConstant]
|
||||
POSSESSIVE_REPEAT_ONE: Final[_NamedIntConstant]
|
||||
RANGE_UNI_IGNORE: Final[_NamedIntConstant]
|
||||
GROUPREF_LOC_IGNORE: Final[_NamedIntConstant]
|
||||
GROUPREF_UNI_IGNORE: Final[_NamedIntConstant]
|
||||
IN_LOC_IGNORE: Final[_NamedIntConstant]
|
||||
IN_UNI_IGNORE: Final[_NamedIntConstant]
|
||||
LITERAL_LOC_IGNORE: Final[_NamedIntConstant]
|
||||
LITERAL_UNI_IGNORE: Final[_NamedIntConstant]
|
||||
NOT_LITERAL_LOC_IGNORE: Final[_NamedIntConstant]
|
||||
NOT_LITERAL_UNI_IGNORE: Final[_NamedIntConstant]
|
||||
MIN_REPEAT: Final[_NamedIntConstant]
|
||||
MAX_REPEAT: Final[_NamedIntConstant]
|
||||
|
||||
# from ATCODES
|
||||
AT_BEGINNING: _NamedIntConstant
|
||||
AT_BEGINNING_LINE: _NamedIntConstant
|
||||
AT_BEGINNING_STRING: _NamedIntConstant
|
||||
AT_BOUNDARY: _NamedIntConstant
|
||||
AT_NON_BOUNDARY: _NamedIntConstant
|
||||
AT_END: _NamedIntConstant
|
||||
AT_END_LINE: _NamedIntConstant
|
||||
AT_END_STRING: _NamedIntConstant
|
||||
AT_LOC_BOUNDARY: _NamedIntConstant
|
||||
AT_LOC_NON_BOUNDARY: _NamedIntConstant
|
||||
AT_UNI_BOUNDARY: _NamedIntConstant
|
||||
AT_UNI_NON_BOUNDARY: _NamedIntConstant
|
||||
AT_BEGINNING: Final[_NamedIntConstant]
|
||||
AT_BEGINNING_LINE: Final[_NamedIntConstant]
|
||||
AT_BEGINNING_STRING: Final[_NamedIntConstant]
|
||||
AT_BOUNDARY: Final[_NamedIntConstant]
|
||||
AT_NON_BOUNDARY: Final[_NamedIntConstant]
|
||||
AT_END: Final[_NamedIntConstant]
|
||||
AT_END_LINE: Final[_NamedIntConstant]
|
||||
AT_END_STRING: Final[_NamedIntConstant]
|
||||
AT_LOC_BOUNDARY: Final[_NamedIntConstant]
|
||||
AT_LOC_NON_BOUNDARY: Final[_NamedIntConstant]
|
||||
AT_UNI_BOUNDARY: Final[_NamedIntConstant]
|
||||
AT_UNI_NON_BOUNDARY: Final[_NamedIntConstant]
|
||||
|
||||
# from CHCODES
|
||||
CATEGORY_DIGIT: _NamedIntConstant
|
||||
CATEGORY_NOT_DIGIT: _NamedIntConstant
|
||||
CATEGORY_SPACE: _NamedIntConstant
|
||||
CATEGORY_NOT_SPACE: _NamedIntConstant
|
||||
CATEGORY_WORD: _NamedIntConstant
|
||||
CATEGORY_NOT_WORD: _NamedIntConstant
|
||||
CATEGORY_LINEBREAK: _NamedIntConstant
|
||||
CATEGORY_NOT_LINEBREAK: _NamedIntConstant
|
||||
CATEGORY_LOC_WORD: _NamedIntConstant
|
||||
CATEGORY_LOC_NOT_WORD: _NamedIntConstant
|
||||
CATEGORY_UNI_DIGIT: _NamedIntConstant
|
||||
CATEGORY_UNI_NOT_DIGIT: _NamedIntConstant
|
||||
CATEGORY_UNI_SPACE: _NamedIntConstant
|
||||
CATEGORY_UNI_NOT_SPACE: _NamedIntConstant
|
||||
CATEGORY_UNI_WORD: _NamedIntConstant
|
||||
CATEGORY_UNI_NOT_WORD: _NamedIntConstant
|
||||
CATEGORY_UNI_LINEBREAK: _NamedIntConstant
|
||||
CATEGORY_UNI_NOT_LINEBREAK: _NamedIntConstant
|
||||
CATEGORY_DIGIT: Final[_NamedIntConstant]
|
||||
CATEGORY_NOT_DIGIT: Final[_NamedIntConstant]
|
||||
CATEGORY_SPACE: Final[_NamedIntConstant]
|
||||
CATEGORY_NOT_SPACE: Final[_NamedIntConstant]
|
||||
CATEGORY_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_NOT_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_LINEBREAK: Final[_NamedIntConstant]
|
||||
CATEGORY_NOT_LINEBREAK: Final[_NamedIntConstant]
|
||||
CATEGORY_LOC_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_LOC_NOT_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_DIGIT: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_NOT_DIGIT: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_SPACE: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_NOT_SPACE: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_NOT_WORD: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_LINEBREAK: Final[_NamedIntConstant]
|
||||
CATEGORY_UNI_NOT_LINEBREAK: Final[_NamedIntConstant]
|
||||
|
||||
@@ -1100,7 +1100,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
|
||||
open: bool = ...,
|
||||
tags: str | list[str] | tuple[str, ...] = ...,
|
||||
) -> None: ...
|
||||
def move(self, item: str | int, parent: str, index: int) -> None: ...
|
||||
def move(self, item: str | int, parent: str, index: int | Literal["end"]) -> None: ...
|
||||
reattach = move
|
||||
def next(self, item: str | int) -> str: ... # returning empty string means last item
|
||||
def parent(self, item: str | int) -> str: ...
|
||||
|
||||
@@ -125,6 +125,9 @@ class Untokenizer:
|
||||
prev_col: int
|
||||
encoding: str | None
|
||||
def add_whitespace(self, start: _Position) -> None: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def add_backslash_continuation(self, start: _Position) -> None: ...
|
||||
|
||||
def untokenize(self, iterable: Iterable[_Token]) -> str: ...
|
||||
def compat(self, token: Sequence[int | str], iterable: Iterable[_Token]) -> None: ...
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
@@ -19,10 +19,10 @@ doctest = false
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
|
||||
|
||||
ruff_db = { workspace = true, default-features = false, features = [] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
|
||||
console_error_panic_hook = { workspace = true, optional = true }
|
||||
|
||||
@@ -198,7 +198,7 @@ pub enum PythonVersion {
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
|
||||
impl From<PythonVersion> for ruff_python_ast::python_version::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
@@ -308,8 +308,8 @@ mod tests {
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
|
||||
red_knot_python_semantic::PythonVersion::default()
|
||||
ruff_python_ast::python_version::PythonVersion::from(PythonVersion::default()),
|
||||
ruff_python_ast::python_version::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -8,13 +8,13 @@ use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::RangedValue;
|
||||
use red_knot_project::watch::{ChangeEvent, ChangedKind};
|
||||
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use ruff_benchmark::criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use ruff_benchmark::TestFile;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
struct Case {
|
||||
|
||||
@@ -4,13 +4,13 @@ use zip::CompressionMethod;
|
||||
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{
|
||||
default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, PythonVersion,
|
||||
SearchPathSettings,
|
||||
default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder};
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
||||
static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::LazyLock::new(|| {
|
||||
let mut builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored);
|
||||
|
||||
@@ -32,3 +32,6 @@ s = set( # outer set comment
|
||||
[ # comprehension comment
|
||||
x for x in range(3)]
|
||||
))))
|
||||
|
||||
# Test trailing comma case
|
||||
s = set([x for x in range(3)],)
|
||||
@@ -174,3 +174,10 @@ class NamesShadowingTypeVarAreNotTouched:
|
||||
type S = int
|
||||
print(S) # not a reference to the type variable, so not touched by the autofix
|
||||
return 42
|
||||
|
||||
|
||||
MetaType = TypeVar("MetaType")
|
||||
|
||||
class MetaTestClass(type):
|
||||
def m(cls: MetaType) -> MetaType:
|
||||
return cls
|
||||
|
||||
@@ -165,3 +165,10 @@ class NoReturnAnnotations:
|
||||
class MultipleBoundParameters:
|
||||
def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
|
||||
|
||||
MetaType = TypeVar("MetaType")
|
||||
|
||||
class MetaTestClass(type):
|
||||
def m(cls: MetaType) -> MetaType:
|
||||
return cls
|
||||
|
||||
@@ -59,3 +59,28 @@ int(+1)
|
||||
int(-1)
|
||||
float(+1.0)
|
||||
float(-1.0)
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15859
|
||||
int(-1) ** 0 # (-1) ** 0
|
||||
2 ** int(-1) # 2 ** -1
|
||||
|
||||
int(-1)[0] # (-1)[0]
|
||||
2[int(-1)] # 2[-1]
|
||||
|
||||
int(-1)(0) # (-1)(0)
|
||||
2(int(-1)) # 2(-1)
|
||||
|
||||
float(-1.0).foo # (-1.0).foo
|
||||
|
||||
await int(-1) # await (-1)
|
||||
|
||||
|
||||
int(+1) ** 0
|
||||
float(+1.0)()
|
||||
|
||||
|
||||
str(
|
||||
'''Lorem
|
||||
ipsum''' # Comment
|
||||
).foo
|
||||
|
||||
@@ -8,7 +8,7 @@ class SetOnceMappingMixin:
|
||||
if key in self:
|
||||
raise KeyError(str(key) + ' already set')
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
|
||||
|
||||
class CaseInsensitiveEnumMeta(EnumMeta):
|
||||
pass
|
||||
@@ -23,6 +23,12 @@ class L(list):
|
||||
class S(str):
|
||||
pass
|
||||
|
||||
class SubscriptDict(dict[str, str]):
|
||||
pass
|
||||
|
||||
class SubscriptList(list[str]):
|
||||
pass
|
||||
|
||||
# currently not detected
|
||||
class SetOnceDict(SetOnceMappingMixin, dict):
|
||||
pass
|
||||
|
||||
29
crates/ruff_linter/resources/test/fixtures/ruff/RUF045.py
vendored
Normal file
29
crates/ruff_linter/resources/test/fixtures/ruff/RUF045.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
from dataclasses import InitVar, KW_ONLY, MISSING, dataclass, field
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
# Errors
|
||||
no_annotation = r"foo"
|
||||
missing = MISSING
|
||||
field = field()
|
||||
|
||||
# No errors
|
||||
__slots__ = ("foo", "bar")
|
||||
__radd__ = __add__
|
||||
_private_attr = 100
|
||||
|
||||
with_annotation: str
|
||||
with_annotation_and_default: int = 42
|
||||
with_annotation_and_field_specifier: bytes = field()
|
||||
|
||||
class_var_no_arguments: ClassVar = 42
|
||||
class_var_with_arguments: ClassVar[int] = 42
|
||||
|
||||
init_var_no_arguments: InitVar = "lorem"
|
||||
init_var_with_arguments: InitVar[str] = "ipsum"
|
||||
|
||||
kw_only: KW_ONLY
|
||||
tu, ple, [unp, ack, ing] = (0, 1, 2, [3, 4, 5])
|
||||
mul, [ti, ple] = (a, ssign), ment = {1: b"3", "2": 4}, [6j, 5]
|
||||
@@ -555,6 +555,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::ClassWithMixedTypeVars) {
|
||||
ruff::rules::class_with_mixed_type_vars(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::ImplicitClassVarInDataclass) {
|
||||
ruff::rules::implicit_class_var_in_dataclass(checker, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
|
||||
@@ -999,6 +999,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
|
||||
(Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral),
|
||||
(Ruff, "043") => (RuleGroup::Preview, rules::ruff::rules::PytestRaisesAmbiguousPattern),
|
||||
(Ruff, "045") => (RuleGroup::Preview, rules::ruff::rules::ImplicitClassVarInDataclass),
|
||||
(Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt),
|
||||
(Ruff, "047") => (RuleGroup::Preview, rules::ruff::rules::NeedlessElse),
|
||||
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
|
||||
|
||||
@@ -33,28 +33,15 @@ pub enum FromCodeError {
|
||||
|
||||
#[derive(EnumIter, Debug, PartialEq, Eq, Clone, Hash, RuleNamespace)]
|
||||
pub enum Linter {
|
||||
/// [Pyflakes](https://pypi.org/project/pyflakes/)
|
||||
#[prefix = "F"]
|
||||
Pyflakes,
|
||||
/// [pycodestyle](https://pypi.org/project/pycodestyle/)
|
||||
#[prefix = "E"]
|
||||
#[prefix = "W"]
|
||||
Pycodestyle,
|
||||
/// [mccabe](https://pypi.org/project/mccabe/)
|
||||
#[prefix = "C90"]
|
||||
McCabe,
|
||||
/// [isort](https://pypi.org/project/isort/)
|
||||
#[prefix = "I"]
|
||||
Isort,
|
||||
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
|
||||
#[prefix = "N"]
|
||||
PEP8Naming,
|
||||
/// [pydocstyle](https://pypi.org/project/pydocstyle/)
|
||||
#[prefix = "D"]
|
||||
Pydocstyle,
|
||||
/// [pyupgrade](https://pypi.org/project/pyupgrade/)
|
||||
#[prefix = "UP"]
|
||||
Pyupgrade,
|
||||
/// [Airflow](https://pypi.org/project/apache-airflow/)
|
||||
#[prefix = "AIR"]
|
||||
Airflow,
|
||||
/// [eradicate](https://pypi.org/project/eradicate/)
|
||||
#[prefix = "ERA"]
|
||||
Eradicate,
|
||||
/// [FastAPI](https://pypi.org/project/fastapi/)
|
||||
#[prefix = "FAST"]
|
||||
FastApi,
|
||||
/// [flake8-2020](https://pypi.org/project/flake8-2020/)
|
||||
#[prefix = "YTT"]
|
||||
Flake82020,
|
||||
@@ -82,12 +69,12 @@ pub enum Linter {
|
||||
/// [flake8-commas](https://pypi.org/project/flake8-commas/)
|
||||
#[prefix = "COM"]
|
||||
Flake8Commas,
|
||||
/// [flake8-copyright](https://pypi.org/project/flake8-copyright/)
|
||||
#[prefix = "CPY"]
|
||||
Flake8Copyright,
|
||||
/// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
|
||||
#[prefix = "C4"]
|
||||
Flake8Comprehensions,
|
||||
/// [flake8-copyright](https://pypi.org/project/flake8-copyright/)
|
||||
#[prefix = "CPY"]
|
||||
Flake8Copyright,
|
||||
/// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
|
||||
#[prefix = "DTZ"]
|
||||
Flake8Datetimez,
|
||||
@@ -103,9 +90,15 @@ pub enum Linter {
|
||||
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
|
||||
#[prefix = "EXE"]
|
||||
Flake8Executable,
|
||||
/// [flake8-fixme](https://github.com/tommilligan/flake8-fixme)
|
||||
#[prefix = "FIX"]
|
||||
Flake8Fixme,
|
||||
/// [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/)
|
||||
#[prefix = "FA"]
|
||||
Flake8FutureAnnotations,
|
||||
/// [flake8-gettext](https://pypi.org/project/flake8-gettext/)
|
||||
#[prefix = "INT"]
|
||||
Flake8GetText,
|
||||
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
||||
#[prefix = "ISC"]
|
||||
Flake8ImplicitStrConcat,
|
||||
@@ -145,72 +138,79 @@ pub enum Linter {
|
||||
/// [flake8-self](https://pypi.org/project/flake8-self/)
|
||||
#[prefix = "SLF"]
|
||||
Flake8Self,
|
||||
/// [flake8-slots](https://pypi.org/project/flake8-slots/)
|
||||
#[prefix = "SLOT"]
|
||||
Flake8Slots,
|
||||
/// [flake8-simplify](https://pypi.org/project/flake8-simplify/)
|
||||
#[prefix = "SIM"]
|
||||
Flake8Simplify,
|
||||
/// [flake8-slots](https://pypi.org/project/flake8-slots/)
|
||||
#[prefix = "SLOT"]
|
||||
Flake8Slots,
|
||||
/// [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
|
||||
#[prefix = "TID"]
|
||||
Flake8TidyImports,
|
||||
/// [flake8-todos](https://github.com/orsinium-labs/flake8-todos/)
|
||||
#[prefix = "TD"]
|
||||
Flake8Todos,
|
||||
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
|
||||
#[prefix = "TC"]
|
||||
Flake8TypeChecking,
|
||||
/// [flake8-gettext](https://pypi.org/project/flake8-gettext/)
|
||||
#[prefix = "INT"]
|
||||
Flake8GetText,
|
||||
/// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/)
|
||||
#[prefix = "ARG"]
|
||||
Flake8UnusedArguments,
|
||||
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
#[prefix = "PTH"]
|
||||
Flake8UsePathlib,
|
||||
/// [flake8-todos](https://github.com/orsinium-labs/flake8-todos/)
|
||||
#[prefix = "TD"]
|
||||
Flake8Todos,
|
||||
/// [flake8-fixme](https://github.com/tommilligan/flake8-fixme)
|
||||
#[prefix = "FIX"]
|
||||
Flake8Fixme,
|
||||
/// [eradicate](https://pypi.org/project/eradicate/)
|
||||
#[prefix = "ERA"]
|
||||
Eradicate,
|
||||
/// [flynt](https://pypi.org/project/flynt/)
|
||||
#[prefix = "FLY"]
|
||||
Flynt,
|
||||
/// [isort](https://pypi.org/project/isort/)
|
||||
#[prefix = "I"]
|
||||
Isort,
|
||||
/// [mccabe](https://pypi.org/project/mccabe/)
|
||||
#[prefix = "C90"]
|
||||
McCabe,
|
||||
/// NumPy-specific rules
|
||||
#[prefix = "NPY"]
|
||||
Numpy,
|
||||
/// [pandas-vet](https://pypi.org/project/pandas-vet/)
|
||||
#[prefix = "PD"]
|
||||
PandasVet,
|
||||
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
|
||||
#[prefix = "N"]
|
||||
PEP8Naming,
|
||||
/// [Perflint](https://pypi.org/project/perflint/)
|
||||
#[prefix = "PERF"]
|
||||
Perflint,
|
||||
/// [pycodestyle](https://pypi.org/project/pycodestyle/)
|
||||
#[prefix = "E"]
|
||||
#[prefix = "W"]
|
||||
Pycodestyle,
|
||||
/// [pydoclint](https://pypi.org/project/pydoclint/)
|
||||
#[prefix = "DOC"]
|
||||
Pydoclint,
|
||||
/// [pydocstyle](https://pypi.org/project/pydocstyle/)
|
||||
#[prefix = "D"]
|
||||
Pydocstyle,
|
||||
/// [Pyflakes](https://pypi.org/project/pyflakes/)
|
||||
#[prefix = "F"]
|
||||
Pyflakes,
|
||||
/// [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks)
|
||||
#[prefix = "PGH"]
|
||||
PygrepHooks,
|
||||
/// [Pylint](https://pypi.org/project/pylint/)
|
||||
#[prefix = "PL"]
|
||||
Pylint,
|
||||
/// [tryceratops](https://pypi.org/project/tryceratops/)
|
||||
#[prefix = "TRY"]
|
||||
Tryceratops,
|
||||
/// [flynt](https://pypi.org/project/flynt/)
|
||||
#[prefix = "FLY"]
|
||||
Flynt,
|
||||
/// NumPy-specific rules
|
||||
#[prefix = "NPY"]
|
||||
Numpy,
|
||||
/// [FastAPI](https://pypi.org/project/fastapi/)
|
||||
#[prefix = "FAST"]
|
||||
FastApi,
|
||||
/// [Airflow](https://pypi.org/project/apache-airflow/)
|
||||
#[prefix = "AIR"]
|
||||
Airflow,
|
||||
/// [Perflint](https://pypi.org/project/perflint/)
|
||||
#[prefix = "PERF"]
|
||||
Perflint,
|
||||
/// [pyupgrade](https://pypi.org/project/pyupgrade/)
|
||||
#[prefix = "UP"]
|
||||
Pyupgrade,
|
||||
/// [refurb](https://pypi.org/project/refurb/)
|
||||
#[prefix = "FURB"]
|
||||
Refurb,
|
||||
/// [pydoclint](https://pypi.org/project/pydoclint/)
|
||||
#[prefix = "DOC"]
|
||||
Pydoclint,
|
||||
/// Ruff-specific rules
|
||||
#[prefix = "RUF"]
|
||||
Ruff,
|
||||
/// [tryceratops](https://pypi.org/project/tryceratops/)
|
||||
#[prefix = "TRY"]
|
||||
Tryceratops,
|
||||
}
|
||||
|
||||
pub trait RuleNamespace: Sized {
|
||||
@@ -430,6 +430,7 @@ pub mod clap_completion {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use std::mem::size_of;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -493,4 +494,19 @@ mod tests {
|
||||
fn rule_size() {
|
||||
assert_eq!(2, size_of::<Rule>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linter_sorting() {
|
||||
let names: Vec<_> = Linter::iter()
|
||||
.map(|linter| linter.name().to_lowercase())
|
||||
.collect();
|
||||
|
||||
let sorted: Vec<_> = names.iter().cloned().sorted().collect();
|
||||
|
||||
assert_eq!(
|
||||
&names[..],
|
||||
&sorted[..],
|
||||
"Linters are not sorted alphabetically (case insensitive)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -125,11 +125,13 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall
|
||||
|
||||
// Replace `)` with `]`.
|
||||
// Place `]` at argument's end or at trailing comma if present
|
||||
let mut tokenizer =
|
||||
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
|
||||
let right_bracket_loc = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.map_or(call.arguments.end(), |comma| comma.end())
|
||||
let after_arg_tokens = checker
|
||||
.tokens()
|
||||
.in_range(TextRange::new(argument.end(), call.end()));
|
||||
let right_bracket_loc = after_arg_tokens
|
||||
.iter()
|
||||
.find(|token| token.kind() == TokenKind::Comma)
|
||||
.map_or(call.arguments.end(), Ranged::end)
|
||||
- TextSize::from(1);
|
||||
let call_end = Edit::replacement("]".to_string(), right_bracket_loc, call.end());
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -128,11 +128,13 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall)
|
||||
|
||||
// Replace `)` with `}`.
|
||||
// Place `}` at argument's end or at trailing comma if present
|
||||
let mut tokenizer =
|
||||
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
|
||||
let right_brace_loc = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.map_or(call.arguments.end(), |comma| comma.end())
|
||||
let after_arg_tokens = checker
|
||||
.tokens()
|
||||
.in_range(TextRange::new(argument.end(), call.end()));
|
||||
let right_brace_loc = after_arg_tokens
|
||||
.iter()
|
||||
.find(|token| token.kind() == TokenKind::Comma)
|
||||
.map_or(call.arguments.end(), Ranged::end)
|
||||
- TextSize::from(1);
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
|
||||
@@ -2,7 +2,8 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
@@ -70,9 +71,18 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast::
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
// Place `}` at argument's end or at trailing comma if present
|
||||
let after_arg_tokens = checker
|
||||
.tokens()
|
||||
.in_range(TextRange::new(argument.end(), call.end()));
|
||||
let right_brace_loc = after_arg_tokens
|
||||
.iter()
|
||||
.find(|token| token.kind() == TokenKind::Comma)
|
||||
.map_or(call.arguments.end() - one, |comma| comma.end() - one);
|
||||
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - one,
|
||||
right_brace_loc,
|
||||
call.end(),
|
||||
);
|
||||
|
||||
|
||||
@@ -292,6 +292,8 @@ C403.py:29:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe
|
||||
33 | | x for x in range(3)]
|
||||
34 | | ))))
|
||||
| |_____^ C403
|
||||
35 |
|
||||
36 | # Test trailing comma case
|
||||
|
|
||||
= help: Rewrite as a set comprehension
|
||||
|
||||
@@ -308,3 +310,21 @@ C403.py:29:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe
|
||||
29 |+s = { # outer set comment
|
||||
30 |+ # comprehension comment
|
||||
31 |+ x for x in range(3)}
|
||||
35 32 |
|
||||
36 33 | # Test trailing comma case
|
||||
37 34 | s = set([x for x in range(3)],)
|
||||
|
||||
C403.py:37:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension)
|
||||
|
|
||||
36 | # Test trailing comma case
|
||||
37 | s = set([x for x in range(3)],)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ C403
|
||||
|
|
||||
= help: Rewrite as a set comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
34 34 | ))))
|
||||
35 35 |
|
||||
36 36 | # Test trailing comma case
|
||||
37 |-s = set([x for x in range(3)],)
|
||||
37 |+s = {x for x in range(3)}
|
||||
|
||||
@@ -4,6 +4,7 @@ use itertools::Itertools;
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::analyze::class::is_metaclass;
|
||||
use ruff_python_semantic::analyze::function_type::{self, FunctionType};
|
||||
use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
|
||||
use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel};
|
||||
@@ -128,9 +129,14 @@ pub(crate) fn custom_type_var_instead_of_self(
|
||||
.next()?;
|
||||
|
||||
let self_or_cls_annotation = self_or_cls_parameter.annotation()?;
|
||||
let parent_class = current_scope.kind.as_class()?;
|
||||
|
||||
// Skip any abstract, static, and overloaded methods.
|
||||
if is_abstract(decorator_list, semantic) || is_overload(decorator_list, semantic) {
|
||||
// Skip any abstract/static/overloaded methods,
|
||||
// and any methods in metaclasses
|
||||
if is_abstract(decorator_list, semantic)
|
||||
|| is_overload(decorator_list, semantic)
|
||||
|| is_metaclass(parent_class, semantic).is_yes()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -661,6 +661,8 @@ PYI019_0.pyi:166:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
166 |- def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
166 |+ def m[T: int](self, other: T) -> Self: ...
|
||||
167 167 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
168 168 |
|
||||
169 169 |
|
||||
|
||||
PYI019_0.pyi:167:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
@@ -677,3 +679,6 @@ PYI019_0.pyi:167:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
166 166 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
167 |- def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
167 |+ def n[T: (int, str)](self, other: T) -> Self: ...
|
||||
168 168 |
|
||||
169 169 |
|
||||
170 170 | MetaType = TypeVar("MetaType")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, BoolOp, Expr, Operator, Stmt, UnaryOp};
|
||||
use ruff_python_ast::{self as ast, Expr, OperatorPrecedence, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -572,167 +572,3 @@ fn in_dunder_method_definition(semantic: &SemanticModel) -> bool {
|
||||
func_def.name.starts_with("__") && func_def.name.ends_with("__")
|
||||
})
|
||||
}
|
||||
|
||||
/// Represents the precedence levels for Python expressions.
|
||||
/// Variants at the top have lower precedence and variants at the bottom have
|
||||
/// higher precedence.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/expressions.html#operator-precedence>
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum OperatorPrecedence {
|
||||
/// The lowest (virtual) precedence level
|
||||
None,
|
||||
/// Precedence of `yield` and `yield from` expressions.
|
||||
Yield,
|
||||
/// Precedence of assignment expressions (`name := expr`).
|
||||
Assign,
|
||||
/// Precedence of starred expressions (`*expr`).
|
||||
Starred,
|
||||
/// Precedence of lambda expressions (`lambda args: expr`).
|
||||
Lambda,
|
||||
/// Precedence of if/else expressions (`expr if cond else expr`).
|
||||
IfElse,
|
||||
/// Precedence of boolean `or` expressions.
|
||||
Or,
|
||||
/// Precedence of boolean `and` expressions.
|
||||
And,
|
||||
/// Precedence of boolean `not` expressions.
|
||||
Not,
|
||||
/// Precedence of comparisons (`<`, `<=`, `>`, `>=`, `!=`, `==`),
|
||||
/// memberships (`in`, `not in`) and identity tests (`is`, `is not`).
|
||||
ComparisonsMembershipIdentity,
|
||||
/// Precedence of bitwise `|` and `^` operators.
|
||||
BitXorOr,
|
||||
/// Precedence of bitwise `&` operator.
|
||||
BitAnd,
|
||||
/// Precedence of left and right shift expressions (`<<`, `>>`).
|
||||
LeftRightShift,
|
||||
/// Precedence of addition and subtraction expressions (`+`, `-`).
|
||||
AddSub,
|
||||
/// Precedence of multiplication (`*`), matrix multiplication (`@`), division (`/`),
|
||||
/// floor division (`//`) and remainder (`%`) expressions.
|
||||
MulDivRemain,
|
||||
/// Precedence of unary positive (`+`), negative (`-`), and bitwise NOT (`~`) expressions.
|
||||
PosNegBitNot,
|
||||
/// Precedence of exponentiation expressions (`**`).
|
||||
Exponent,
|
||||
/// Precedence of `await` expressions.
|
||||
Await,
|
||||
/// Precedence of call expressions (`()`), attribute access (`.`), and subscript (`[]`) expressions.
|
||||
CallAttribute,
|
||||
/// Precedence of atomic expressions (literals, names, containers).
|
||||
Atomic,
|
||||
}
|
||||
|
||||
impl OperatorPrecedence {
|
||||
fn from_expr(expr: &Expr) -> Self {
|
||||
match expr {
|
||||
// Binding or parenthesized expression, list display, dictionary display, set display
|
||||
Expr::Tuple(_)
|
||||
| Expr::Dict(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::List(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_)
|
||||
| Expr::Generator(_)
|
||||
| Expr::Name(_)
|
||||
| Expr::StringLiteral(_)
|
||||
| Expr::BytesLiteral(_)
|
||||
| Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
| Expr::EllipsisLiteral(_)
|
||||
| Expr::FString(_) => Self::Atomic,
|
||||
// Subscription, slicing, call, attribute reference
|
||||
Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_) | Expr::Slice(_) => {
|
||||
Self::CallAttribute
|
||||
}
|
||||
|
||||
// Await expression
|
||||
Expr::Await(_) => Self::Await,
|
||||
|
||||
// Exponentiation **
|
||||
// Handled below along with other binary operators
|
||||
|
||||
// Unary operators: +x, -x, ~x (except boolean not)
|
||||
Expr::UnaryOp(operator) => match operator.op {
|
||||
UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert => Self::PosNegBitNot,
|
||||
UnaryOp::Not => Self::Not,
|
||||
},
|
||||
|
||||
// Math binary ops
|
||||
Expr::BinOp(binary_operation) => Self::from(binary_operation.op),
|
||||
|
||||
// Comparisons: <, <=, >, >=, ==, !=, in, not in, is, is not
|
||||
Expr::Compare(_) => Self::ComparisonsMembershipIdentity,
|
||||
|
||||
// Boolean not
|
||||
// Handled above in unary operators
|
||||
|
||||
// Boolean operations: and, or
|
||||
Expr::BoolOp(bool_op) => Self::from(bool_op.op),
|
||||
|
||||
// Conditional expressions: x if y else z
|
||||
Expr::If(_) => Self::IfElse,
|
||||
|
||||
// Lambda expressions
|
||||
Expr::Lambda(_) => Self::Lambda,
|
||||
|
||||
// Unpacking also omitted in the docs, but has almost the lowest precedence,
|
||||
// except for assignment & yield expressions. E.g. `[*(v := [1,2])]` is valid
|
||||
// but `[*v := [1,2]] would fail on incorrect syntax because * will associate
|
||||
// `v` before the assignment.
|
||||
Expr::Starred(_) => Self::Starred,
|
||||
|
||||
// Assignment expressions (aka named)
|
||||
Expr::Named(_) => Self::Assign,
|
||||
|
||||
// Although omitted in docs, yield expressions may be used inside an expression
|
||||
// but must be parenthesized. So for our purposes we assume they just have
|
||||
// the lowest "real" precedence.
|
||||
Expr::Yield(_) | Expr::YieldFrom(_) => Self::Yield,
|
||||
|
||||
// Not a real python expression, so treat as lowest as well
|
||||
Expr::IpyEscapeCommand(_) => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Expr> for OperatorPrecedence {
|
||||
fn from(expr: &Expr) -> Self {
|
||||
Self::from_expr(expr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Operator> for OperatorPrecedence {
|
||||
fn from(operator: Operator) -> Self {
|
||||
match operator {
|
||||
// Multiplication, matrix multiplication, division, floor division, remainder:
|
||||
// *, @, /, //, %
|
||||
Operator::Mult
|
||||
| Operator::MatMult
|
||||
| Operator::Div
|
||||
| Operator::Mod
|
||||
| Operator::FloorDiv => Self::MulDivRemain,
|
||||
// Addition, subtraction
|
||||
Operator::Add | Operator::Sub => Self::AddSub,
|
||||
// Bitwise shifts: <<, >>
|
||||
Operator::LShift | Operator::RShift => Self::LeftRightShift,
|
||||
// Bitwise operations: &, ^, |
|
||||
Operator::BitAnd => Self::BitAnd,
|
||||
Operator::BitXor | Operator::BitOr => Self::BitXorOr,
|
||||
// Exponentiation **
|
||||
Operator::Pow => Self::Exponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BoolOp> for OperatorPrecedence {
|
||||
fn from(operator: BoolOp) -> Self {
|
||||
match operator {
|
||||
BoolOp::And => Self::And,
|
||||
BoolOp::Or => Self::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, UnaryOp};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, OperatorPrecedence, UnaryOp};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -113,6 +113,9 @@ impl fmt::Display for LiteralType {
|
||||
/// "foo"
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is marked as unsafe if it might remove comments.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `str`](https://docs.python.org/3/library/stdtypes.html#str)
|
||||
/// - [Python documentation: `bytes`](https://docs.python.org/3/library/stdtypes.html#bytes)
|
||||
@@ -205,12 +208,12 @@ pub(crate) fn native_literals(
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
Some(arg) => {
|
||||
let literal_expr = if let Some(literal_expr) = arg.as_literal_expr() {
|
||||
let (has_unary_op, literal_expr) = if let Some(literal_expr) = arg.as_literal_expr() {
|
||||
// Skip implicit concatenated strings.
|
||||
if literal_expr.is_implicit_concatenated() {
|
||||
return;
|
||||
}
|
||||
literal_expr
|
||||
(false, literal_expr)
|
||||
} else if let Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::UAdd | UnaryOp::USub,
|
||||
operand,
|
||||
@@ -221,7 +224,7 @@ pub(crate) fn native_literals(
|
||||
.as_literal_expr()
|
||||
.filter(|expr| matches!(expr, LiteralExpressionRef::NumberLiteral(_)))
|
||||
{
|
||||
literal_expr
|
||||
(true, literal_expr)
|
||||
} else {
|
||||
// Only allow unary operators for numbers.
|
||||
return;
|
||||
@@ -240,21 +243,34 @@ pub(crate) fn native_literals(
|
||||
|
||||
let arg_code = checker.locator().slice(arg);
|
||||
|
||||
// Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float
|
||||
// Ex) `(7).denominator` is valid but `7.denominator` is not
|
||||
// Note that floats do not have this problem
|
||||
// Ex) `(1.0).real` is valid and `1.0.real` is too
|
||||
let content = match (parent_expr, literal_type) {
|
||||
(Some(Expr::Attribute(_)), LiteralType::Int) => format!("({arg_code})"),
|
||||
let content = match (parent_expr, literal_type, has_unary_op) {
|
||||
// Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float
|
||||
// Ex) `(7).denominator` is valid but `7.denominator` is not
|
||||
// Note that floats do not have this problem
|
||||
// Ex) `(1.0).real` is valid and `1.0.real` is too
|
||||
(Some(Expr::Attribute(_)), LiteralType::Int, _) => format!("({arg_code})"),
|
||||
|
||||
(Some(parent), _, _) => {
|
||||
if OperatorPrecedence::from(parent) > OperatorPrecedence::from(arg) {
|
||||
format!("({arg_code})")
|
||||
} else {
|
||||
arg_code.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
_ => arg_code.to_string(),
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
content,
|
||||
call.range(),
|
||||
)));
|
||||
checker.report_diagnostic(diagnostic);
|
||||
let applicability = if checker.comment_ranges().intersects(call.range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
let edit = Edit::range_replacement(content, call.range());
|
||||
let fix = Fix::applicable_edit(edit, applicability);
|
||||
|
||||
let diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range());
|
||||
checker.report_diagnostic(diagnostic.with_fix(fix));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,7 @@ UP018.py:59:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
59 |+-1
|
||||
60 60 | float(+1.0)
|
||||
61 61 | float(-1.0)
|
||||
62 62 |
|
||||
|
||||
UP018.py:60:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
@@ -376,6 +377,8 @@ UP018.py:60:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
60 |-float(+1.0)
|
||||
60 |++1.0
|
||||
61 61 | float(-1.0)
|
||||
62 62 |
|
||||
63 63 |
|
||||
|
||||
UP018.py:61:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
@@ -392,3 +395,223 @@ UP018.py:61:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
60 60 | float(+1.0)
|
||||
61 |-float(-1.0)
|
||||
61 |+-1.0
|
||||
62 62 |
|
||||
63 63 |
|
||||
64 64 | # https://github.com/astral-sh/ruff/issues/15859
|
||||
|
||||
UP018.py:65:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
64 | # https://github.com/astral-sh/ruff/issues/15859
|
||||
65 | int(-1) ** 0 # (-1) ** 0
|
||||
| ^^^^^^^ UP018
|
||||
66 | 2 ** int(-1) # 2 ** -1
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
62 62 |
|
||||
63 63 |
|
||||
64 64 | # https://github.com/astral-sh/ruff/issues/15859
|
||||
65 |-int(-1) ** 0 # (-1) ** 0
|
||||
65 |+(-1) ** 0 # (-1) ** 0
|
||||
66 66 | 2 ** int(-1) # 2 ** -1
|
||||
67 67 |
|
||||
68 68 | int(-1)[0] # (-1)[0]
|
||||
|
||||
UP018.py:66:6: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
64 | # https://github.com/astral-sh/ruff/issues/15859
|
||||
65 | int(-1) ** 0 # (-1) ** 0
|
||||
66 | 2 ** int(-1) # 2 ** -1
|
||||
| ^^^^^^^ UP018
|
||||
67 |
|
||||
68 | int(-1)[0] # (-1)[0]
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
63 63 |
|
||||
64 64 | # https://github.com/astral-sh/ruff/issues/15859
|
||||
65 65 | int(-1) ** 0 # (-1) ** 0
|
||||
66 |-2 ** int(-1) # 2 ** -1
|
||||
66 |+2 ** (-1) # 2 ** -1
|
||||
67 67 |
|
||||
68 68 | int(-1)[0] # (-1)[0]
|
||||
69 69 | 2[int(-1)] # 2[-1]
|
||||
|
||||
UP018.py:68:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
66 | 2 ** int(-1) # 2 ** -1
|
||||
67 |
|
||||
68 | int(-1)[0] # (-1)[0]
|
||||
| ^^^^^^^ UP018
|
||||
69 | 2[int(-1)] # 2[-1]
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | int(-1) ** 0 # (-1) ** 0
|
||||
66 66 | 2 ** int(-1) # 2 ** -1
|
||||
67 67 |
|
||||
68 |-int(-1)[0] # (-1)[0]
|
||||
68 |+(-1)[0] # (-1)[0]
|
||||
69 69 | 2[int(-1)] # 2[-1]
|
||||
70 70 |
|
||||
71 71 | int(-1)(0) # (-1)(0)
|
||||
|
||||
UP018.py:69:3: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
68 | int(-1)[0] # (-1)[0]
|
||||
69 | 2[int(-1)] # 2[-1]
|
||||
| ^^^^^^^ UP018
|
||||
70 |
|
||||
71 | int(-1)(0) # (-1)(0)
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | 2 ** int(-1) # 2 ** -1
|
||||
67 67 |
|
||||
68 68 | int(-1)[0] # (-1)[0]
|
||||
69 |-2[int(-1)] # 2[-1]
|
||||
69 |+2[(-1)] # 2[-1]
|
||||
70 70 |
|
||||
71 71 | int(-1)(0) # (-1)(0)
|
||||
72 72 | 2(int(-1)) # 2(-1)
|
||||
|
||||
UP018.py:71:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
69 | 2[int(-1)] # 2[-1]
|
||||
70 |
|
||||
71 | int(-1)(0) # (-1)(0)
|
||||
| ^^^^^^^ UP018
|
||||
72 | 2(int(-1)) # 2(-1)
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
68 68 | int(-1)[0] # (-1)[0]
|
||||
69 69 | 2[int(-1)] # 2[-1]
|
||||
70 70 |
|
||||
71 |-int(-1)(0) # (-1)(0)
|
||||
71 |+(-1)(0) # (-1)(0)
|
||||
72 72 | 2(int(-1)) # 2(-1)
|
||||
73 73 |
|
||||
74 74 | float(-1.0).foo # (-1.0).foo
|
||||
|
||||
UP018.py:72:3: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
71 | int(-1)(0) # (-1)(0)
|
||||
72 | 2(int(-1)) # 2(-1)
|
||||
| ^^^^^^^ UP018
|
||||
73 |
|
||||
74 | float(-1.0).foo # (-1.0).foo
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
69 69 | 2[int(-1)] # 2[-1]
|
||||
70 70 |
|
||||
71 71 | int(-1)(0) # (-1)(0)
|
||||
72 |-2(int(-1)) # 2(-1)
|
||||
72 |+2((-1)) # 2(-1)
|
||||
73 73 |
|
||||
74 74 | float(-1.0).foo # (-1.0).foo
|
||||
75 75 |
|
||||
|
||||
UP018.py:74:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
72 | 2(int(-1)) # 2(-1)
|
||||
73 |
|
||||
74 | float(-1.0).foo # (-1.0).foo
|
||||
| ^^^^^^^^^^^ UP018
|
||||
75 |
|
||||
76 | await int(-1) # await (-1)
|
||||
|
|
||||
= help: Replace with float literal
|
||||
|
||||
ℹ Safe fix
|
||||
71 71 | int(-1)(0) # (-1)(0)
|
||||
72 72 | 2(int(-1)) # 2(-1)
|
||||
73 73 |
|
||||
74 |-float(-1.0).foo # (-1.0).foo
|
||||
74 |+(-1.0).foo # (-1.0).foo
|
||||
75 75 |
|
||||
76 76 | await int(-1) # await (-1)
|
||||
77 77 |
|
||||
|
||||
UP018.py:76:7: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
74 | float(-1.0).foo # (-1.0).foo
|
||||
75 |
|
||||
76 | await int(-1) # await (-1)
|
||||
| ^^^^^^^ UP018
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 |
|
||||
74 74 | float(-1.0).foo # (-1.0).foo
|
||||
75 75 |
|
||||
76 |-await int(-1) # await (-1)
|
||||
76 |+await (-1) # await (-1)
|
||||
77 77 |
|
||||
78 78 |
|
||||
79 79 | int(+1) ** 0
|
||||
|
||||
UP018.py:79:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
79 | int(+1) ** 0
|
||||
| ^^^^^^^ UP018
|
||||
80 | float(+1.0)()
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
76 76 | await int(-1) # await (-1)
|
||||
77 77 |
|
||||
78 78 |
|
||||
79 |-int(+1) ** 0
|
||||
79 |+(+1) ** 0
|
||||
80 80 | float(+1.0)()
|
||||
81 81 |
|
||||
82 82 |
|
||||
|
||||
UP018.py:80:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
79 | int(+1) ** 0
|
||||
80 | float(+1.0)()
|
||||
| ^^^^^^^^^^^ UP018
|
||||
|
|
||||
= help: Replace with float literal
|
||||
|
||||
ℹ Safe fix
|
||||
77 77 |
|
||||
78 78 |
|
||||
79 79 | int(+1) ** 0
|
||||
80 |-float(+1.0)()
|
||||
80 |+(+1.0)()
|
||||
81 81 |
|
||||
82 82 |
|
||||
83 83 | str(
|
||||
|
||||
UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
||||
|
|
||||
83 | / str(
|
||||
84 | | '''Lorem
|
||||
85 | | ipsum''' # Comment
|
||||
86 | | ).foo
|
||||
| |_^ UP018
|
||||
|
|
||||
= help: Replace with string literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
80 80 | float(+1.0)()
|
||||
81 81 |
|
||||
82 82 |
|
||||
83 |-str(
|
||||
84 |- '''Lorem
|
||||
85 |- ipsum''' # Comment
|
||||
86 |-).foo
|
||||
83 |+'''Lorem
|
||||
84 |+ ipsum'''.foo
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Arguments, StmtClassDef};
|
||||
use ruff_python_ast::{helpers::map_subscript, Arguments, StmtClassDef};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
@@ -70,11 +70,16 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
|
||||
return;
|
||||
};
|
||||
|
||||
// Expect only one base class else return
|
||||
let [base] = &**bases else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(symbol) = checker.semantic().resolve_builtin_symbol(base) else {
|
||||
// Check if the base class is a subscript expression so that only the name expr
|
||||
// is checked and modified.
|
||||
let base_expr = map_subscript(base);
|
||||
|
||||
let Some(symbol) = checker.semantic().resolve_builtin_symbol(base_expr) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -89,7 +94,7 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
|
||||
subclass: symbol.to_string(),
|
||||
replacement: user_symbol.to_string(),
|
||||
},
|
||||
base.range(),
|
||||
base_expr.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
@@ -97,7 +102,7 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
|
||||
base.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let other_edit = Edit::range_replacement(binding, base.range());
|
||||
let other_edit = Edit::range_replacement(binding, base_expr.range());
|
||||
Ok(Fix::unsafe_edits(import_edit, [other_edit]))
|
||||
});
|
||||
checker.report_diagnostic(diagnostic);
|
||||
|
||||
@@ -74,4 +74,52 @@ FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collecti
|
||||
23 |+class S(UserString):
|
||||
24 24 | pass
|
||||
25 25 |
|
||||
26 26 | # currently not detected
|
||||
26 26 | class SubscriptDict(dict[str, str]):
|
||||
|
||||
FURB189.py:26:21: FURB189 [*] Subclassing `dict` can be error prone, use `collections.UserDict` instead
|
||||
|
|
||||
24 | pass
|
||||
25 |
|
||||
26 | class SubscriptDict(dict[str, str]):
|
||||
| ^^^^ FURB189
|
||||
27 | pass
|
||||
|
|
||||
= help: Replace with `collections.UserDict`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | # setup
|
||||
2 2 | from enum import Enum, EnumMeta
|
||||
3 |-from collections import UserList as UL
|
||||
3 |+from collections import UserList as UL, UserDict
|
||||
4 4 |
|
||||
5 5 | class SetOnceMappingMixin:
|
||||
6 6 | __slots__ = ()
|
||||
--------------------------------------------------------------------------------
|
||||
23 23 | class S(str):
|
||||
24 24 | pass
|
||||
25 25 |
|
||||
26 |-class SubscriptDict(dict[str, str]):
|
||||
26 |+class SubscriptDict(UserDict[str, str]):
|
||||
27 27 | pass
|
||||
28 28 |
|
||||
29 29 | class SubscriptList(list[str]):
|
||||
|
||||
FURB189.py:29:21: FURB189 [*] Subclassing `list` can be error prone, use `collections.UserList` instead
|
||||
|
|
||||
27 | pass
|
||||
28 |
|
||||
29 | class SubscriptList(list[str]):
|
||||
| ^^^^ FURB189
|
||||
30 | pass
|
||||
|
|
||||
= help: Replace with `collections.UserList`
|
||||
|
||||
ℹ Unsafe fix
|
||||
26 26 | class SubscriptDict(dict[str, str]):
|
||||
27 27 | pass
|
||||
28 28 |
|
||||
29 |-class SubscriptList(list[str]):
|
||||
29 |+class SubscriptList(UL[str]):
|
||||
30 30 | pass
|
||||
31 31 |
|
||||
32 32 | # currently not detected
|
||||
|
||||
@@ -436,6 +436,7 @@ mod tests {
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
|
||||
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::helpers::is_dunder;
|
||||
use ruff_python_ast::{Expr, ExprName, Stmt, StmtAssign, StmtClassDef};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::ruff::rules::helpers::{dataclass_kind, DataclassKind};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for implicit class variables in dataclasses.
|
||||
///
|
||||
/// Variables matching the [`lint.dummy-variable-rgx`] are excluded
|
||||
/// from this rule.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Class variables are shared between all instances of that class.
|
||||
/// In dataclasses, fields with no annotations at all
|
||||
/// are implicitly considered class variables, and a `TypeError` is
|
||||
/// raised if a user attempts to initialize an instance of the class
|
||||
/// with this field.
|
||||
///
|
||||
///
|
||||
/// ```python
|
||||
/// @dataclass
|
||||
/// class C:
|
||||
/// a = 1
|
||||
/// b: str = ""
|
||||
///
|
||||
/// C(a = 42) # TypeError: C.__init__() got an unexpected keyword argument 'a'
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// @dataclass
|
||||
/// class C:
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import ClassVar
|
||||
///
|
||||
///
|
||||
/// @dataclass
|
||||
/// class C:
|
||||
/// a: ClassVar[int] = 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - [`lint.dummy-variable-rgx`]
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ImplicitClassVarInDataclass;
|
||||
|
||||
impl Violation for ImplicitClassVarInDataclass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Assignment without annotation found in dataclass body".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Use `ClassVar[...]`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF045
|
||||
pub(crate) fn implicit_class_var_in_dataclass(checker: &mut Checker, class_def: &StmtClassDef) {
|
||||
let dataclass_kind = dataclass_kind(class_def, checker.semantic());
|
||||
|
||||
if !matches!(dataclass_kind, Some((DataclassKind::Stdlib, _))) {
|
||||
return;
|
||||
};
|
||||
|
||||
for statement in &class_def.body {
|
||||
let Stmt::Assign(StmtAssign { targets, .. }) = statement else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if targets.len() > 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let target = targets.first().unwrap();
|
||||
let Expr::Name(ExprName { id, .. }) = target else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if checker.settings.dummy_variable_rgx.is_match(id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_dunder(id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let diagnostic = Diagnostic::new(ImplicitClassVarInDataclass, target.range());
|
||||
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub(crate) use explicit_f_string_type_conversion::*;
|
||||
pub(crate) use falsy_dict_get_fallback::*;
|
||||
pub(crate) use function_call_in_dataclass_default::*;
|
||||
pub(crate) use if_key_in_dict_del::*;
|
||||
pub(crate) use implicit_classvar_in_dataclass::*;
|
||||
pub(crate) use implicit_optional::*;
|
||||
pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*;
|
||||
pub(crate) use indented_form_feed::*;
|
||||
@@ -68,6 +69,7 @@ mod falsy_dict_get_fallback;
|
||||
mod function_call_in_dataclass_default;
|
||||
mod helpers;
|
||||
mod if_key_in_dict_del;
|
||||
mod implicit_classvar_in_dataclass;
|
||||
mod implicit_optional;
|
||||
mod incorrectly_parenthesized_tuple_in_subscript;
|
||||
mod indented_form_feed;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF045.py:8:5: RUF045 Assignment without annotation found in dataclass body
|
||||
|
|
||||
6 | class C:
|
||||
7 | # Errors
|
||||
8 | no_annotation = r"foo"
|
||||
| ^^^^^^^^^^^^^ RUF045
|
||||
9 | missing = MISSING
|
||||
10 | field = field()
|
||||
|
|
||||
= help: Use `ClassVar[...]`
|
||||
|
||||
RUF045.py:9:5: RUF045 Assignment without annotation found in dataclass body
|
||||
|
|
||||
7 | # Errors
|
||||
8 | no_annotation = r"foo"
|
||||
9 | missing = MISSING
|
||||
| ^^^^^^^ RUF045
|
||||
10 | field = field()
|
||||
|
|
||||
= help: Use `ClassVar[...]`
|
||||
|
||||
RUF045.py:10:5: RUF045 Assignment without annotation found in dataclass body
|
||||
|
|
||||
8 | no_annotation = r"foo"
|
||||
9 | missing = MISSING
|
||||
10 | field = field()
|
||||
| ^^^^^ RUF045
|
||||
11 |
|
||||
12 | # No errors
|
||||
|
|
||||
= help: Use `ClassVar[...]`
|
||||
@@ -139,12 +139,9 @@ def write_owned_enum(out: list[str], ast: Ast) -> None:
|
||||
out.append("")
|
||||
if group.rustdoc is not None:
|
||||
out.append(group.rustdoc)
|
||||
out.append("#[derive(Clone, Debug, PartialEq, is_macro::Is)]")
|
||||
out.append("#[derive(Clone, Debug, PartialEq)]")
|
||||
out.append(f"pub enum {group.owned_enum_ty} {{")
|
||||
for node in group.nodes:
|
||||
if group.add_suffix_to_is_methods:
|
||||
is_name = to_snake_case(node.variant + group.name)
|
||||
out.append(f'#[is(name = "{is_name}")]')
|
||||
out.append(f"{node.variant}({node.ty}),")
|
||||
out.append("}")
|
||||
|
||||
@@ -170,6 +167,93 @@ def write_owned_enum(out: list[str], ast: Ast) -> None:
|
||||
}
|
||||
""")
|
||||
|
||||
out.append(
|
||||
"#[allow(dead_code, clippy::match_wildcard_for_single_variants)]"
|
||||
) # Not all is_methods are used
|
||||
out.append(f"impl {group.name} {{")
|
||||
for node in group.nodes:
|
||||
is_name = to_snake_case(node.variant)
|
||||
variant_name = node.variant
|
||||
match_arm = f"Self::{variant_name}"
|
||||
if group.add_suffix_to_is_methods:
|
||||
is_name = to_snake_case(node.variant + group.name)
|
||||
if len(group.nodes) > 1:
|
||||
out.append(f"""
|
||||
#[inline]
|
||||
pub const fn is_{is_name}(&self) -> bool {{
|
||||
matches!(self, {match_arm}(_))
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn {is_name}(self) -> Option<{node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
_ => None,
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn expect_{is_name}(self) -> {node.ty} {{
|
||||
match self {{
|
||||
{match_arm}(val) => val,
|
||||
_ => panic!("called expect on {{self:?}}"),
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn as_{is_name}_mut(&mut self) -> Option<&mut {node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
_ => None,
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn as_{is_name}(&self) -> Option<&{node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
_ => None,
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
elif len(group.nodes) == 1:
|
||||
out.append(f"""
|
||||
#[inline]
|
||||
pub const fn is_{is_name}(&self) -> bool {{
|
||||
matches!(self, {match_arm}(_))
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn {is_name}(self) -> Option<{node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn expect_{is_name}(self) -> {node.ty} {{
|
||||
match self {{
|
||||
{match_arm}(val) => val,
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn as_{is_name}_mut(&mut self) -> Option<&mut {node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
}}
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
pub fn as_{is_name}(&self) -> Option<&{node.ty}> {{
|
||||
match self {{
|
||||
{match_arm}(val) => Some(val),
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
out.append("}")
|
||||
|
||||
for node in ast.all_nodes:
|
||||
out.append(f"""
|
||||
impl ruff_text_size::Ranged for {node.ty} {{
|
||||
|
||||
2789
crates/ruff_python_ast/src/generated.rs
generated
2789
crates/ruff_python_ast/src/generated.rs
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ pub use expression::*;
|
||||
pub use generated::*;
|
||||
pub use int::*;
|
||||
pub use nodes::*;
|
||||
pub use operator_precedence::*;
|
||||
|
||||
pub mod comparable;
|
||||
pub mod docstrings;
|
||||
@@ -16,7 +17,9 @@ mod int;
|
||||
pub mod name;
|
||||
mod node;
|
||||
mod nodes;
|
||||
pub mod operator_precedence;
|
||||
pub mod parenthesize;
|
||||
pub mod python_version;
|
||||
pub mod relocate;
|
||||
pub mod script;
|
||||
pub mod statement_visitor;
|
||||
|
||||
@@ -18,7 +18,8 @@ use crate::{
|
||||
name::Name,
|
||||
str::{Quote, TripleQuotes},
|
||||
str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix},
|
||||
ExceptHandler, Expr, FStringElement, LiteralExpressionRef, Pattern, Stmt, TypeParam,
|
||||
ExceptHandler, Expr, ExprRef, FStringElement, LiteralExpressionRef, OperatorPrecedence,
|
||||
Pattern, Stmt, TypeParam,
|
||||
};
|
||||
|
||||
/// See also [Module](https://docs.python.org/3/library/ast.html#ast.Module)
|
||||
@@ -365,6 +366,17 @@ impl Expr {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`OperatorPrecedence`] of this expression
|
||||
pub fn precedence(&self) -> OperatorPrecedence {
|
||||
OperatorPrecedence::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprRef<'_> {
|
||||
pub fn precedence(&self) -> OperatorPrecedence {
|
||||
OperatorPrecedence::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST node used to represent a IPython escape command at the expression level.
|
||||
|
||||
176
crates/ruff_python_ast/src/operator_precedence.rs
Normal file
176
crates/ruff_python_ast/src/operator_precedence.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::{BoolOp, Expr, ExprRef, Operator, UnaryOp};
|
||||
|
||||
/// Represents the precedence levels for Python expressions.
|
||||
/// Variants at the top have lower precedence and variants at the bottom have
|
||||
/// higher precedence.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/expressions.html#operator-precedence>
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum OperatorPrecedence {
|
||||
/// The lowest (virtual) precedence level
|
||||
None,
|
||||
/// Precedence of `yield` and `yield from` expressions.
|
||||
Yield,
|
||||
/// Precedence of assignment expressions (`name := expr`).
|
||||
Assign,
|
||||
/// Precedence of starred expressions (`*expr`).
|
||||
Starred,
|
||||
/// Precedence of lambda expressions (`lambda args: expr`).
|
||||
Lambda,
|
||||
/// Precedence of if/else expressions (`expr if cond else expr`).
|
||||
IfElse,
|
||||
/// Precedence of boolean `or` expressions.
|
||||
Or,
|
||||
/// Precedence of boolean `and` expressions.
|
||||
And,
|
||||
/// Precedence of boolean `not` expressions.
|
||||
Not,
|
||||
/// Precedence of comparisons (`<`, `<=`, `>`, `>=`, `!=`, `==`),
|
||||
/// memberships (`in`, `not in`) and identity tests (`is`, `is not`).
|
||||
ComparisonsMembershipIdentity,
|
||||
/// Precedence of bitwise `|` and `^` operators.
|
||||
BitXorOr,
|
||||
/// Precedence of bitwise `&` operator.
|
||||
BitAnd,
|
||||
/// Precedence of left and right shift expressions (`<<`, `>>`).
|
||||
LeftRightShift,
|
||||
/// Precedence of addition and subtraction expressions (`+`, `-`).
|
||||
AddSub,
|
||||
/// Precedence of multiplication (`*`), matrix multiplication (`@`), division (`/`),
|
||||
/// floor division (`//`) and remainder (`%`) expressions.
|
||||
MulDivRemain,
|
||||
/// Precedence of unary positive (`+`), negative (`-`), and bitwise NOT (`~`) expressions.
|
||||
PosNegBitNot,
|
||||
/// Precedence of exponentiation expressions (`**`).
|
||||
Exponent,
|
||||
/// Precedence of `await` expressions.
|
||||
Await,
|
||||
/// Precedence of call expressions (`()`), attribute access (`.`), and subscript (`[]`) expressions.
|
||||
CallAttribute,
|
||||
/// Precedence of atomic expressions (literals, names, containers).
|
||||
Atomic,
|
||||
}
|
||||
|
||||
impl OperatorPrecedence {
|
||||
pub fn from_expr_ref(expr: &ExprRef) -> Self {
|
||||
match expr {
|
||||
// Binding or parenthesized expression, list display, dictionary display, set display
|
||||
ExprRef::Tuple(_)
|
||||
| ExprRef::Dict(_)
|
||||
| ExprRef::Set(_)
|
||||
| ExprRef::ListComp(_)
|
||||
| ExprRef::List(_)
|
||||
| ExprRef::SetComp(_)
|
||||
| ExprRef::DictComp(_)
|
||||
| ExprRef::Generator(_)
|
||||
| ExprRef::Name(_)
|
||||
| ExprRef::StringLiteral(_)
|
||||
| ExprRef::BytesLiteral(_)
|
||||
| ExprRef::NumberLiteral(_)
|
||||
| ExprRef::BooleanLiteral(_)
|
||||
| ExprRef::NoneLiteral(_)
|
||||
| ExprRef::EllipsisLiteral(_)
|
||||
| ExprRef::FString(_) => Self::Atomic,
|
||||
// Subscription, slicing, call, attribute reference
|
||||
ExprRef::Attribute(_)
|
||||
| ExprRef::Subscript(_)
|
||||
| ExprRef::Call(_)
|
||||
| ExprRef::Slice(_) => Self::CallAttribute,
|
||||
|
||||
// Await expression
|
||||
ExprRef::Await(_) => Self::Await,
|
||||
|
||||
// Exponentiation **
|
||||
// Handled below along with other binary operators
|
||||
|
||||
// Unary operators: +x, -x, ~x (except boolean not)
|
||||
ExprRef::UnaryOp(operator) => match operator.op {
|
||||
UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert => Self::PosNegBitNot,
|
||||
UnaryOp::Not => Self::Not,
|
||||
},
|
||||
|
||||
// Math binary ops
|
||||
ExprRef::BinOp(binary_operation) => Self::from(binary_operation.op),
|
||||
|
||||
// Comparisons: <, <=, >, >=, ==, !=, in, not in, is, is not
|
||||
ExprRef::Compare(_) => Self::ComparisonsMembershipIdentity,
|
||||
|
||||
// Boolean not
|
||||
// Handled above in unary operators
|
||||
|
||||
// Boolean operations: and, or
|
||||
ExprRef::BoolOp(bool_op) => Self::from(bool_op.op),
|
||||
|
||||
// Conditional expressions: x if y else z
|
||||
ExprRef::If(_) => Self::IfElse,
|
||||
|
||||
// Lambda expressions
|
||||
ExprRef::Lambda(_) => Self::Lambda,
|
||||
|
||||
// Unpacking also omitted in the docs, but has almost the lowest precedence,
|
||||
// except for assignment & yield expressions. E.g. `[*(v := [1,2])]` is valid
|
||||
// but `[*v := [1,2]] would fail on incorrect syntax because * will associate
|
||||
// `v` before the assignment.
|
||||
ExprRef::Starred(_) => Self::Starred,
|
||||
|
||||
// Assignment expressions (aka named)
|
||||
ExprRef::Named(_) => Self::Assign,
|
||||
|
||||
// Although omitted in docs, yield expressions may be used inside an expression
|
||||
// but must be parenthesized. So for our purposes we assume they just have
|
||||
// the lowest "real" precedence.
|
||||
ExprRef::Yield(_) | ExprRef::YieldFrom(_) => Self::Yield,
|
||||
|
||||
// Not a real python expression, so treat as lowest as well
|
||||
ExprRef::IpyEscapeCommand(_) => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_expr(expr: &Expr) -> Self {
|
||||
Self::from(&ExprRef::from(expr))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Expr> for OperatorPrecedence {
|
||||
fn from(expr: &Expr) -> Self {
|
||||
Self::from_expr(expr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&ExprRef<'a>> for OperatorPrecedence {
|
||||
fn from(expr_ref: &ExprRef<'a>) -> Self {
|
||||
Self::from_expr_ref(expr_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Operator> for OperatorPrecedence {
|
||||
fn from(operator: Operator) -> Self {
|
||||
match operator {
|
||||
// Multiplication, matrix multiplication, division, floor division, remainder:
|
||||
// *, @, /, //, %
|
||||
Operator::Mult
|
||||
| Operator::MatMult
|
||||
| Operator::Div
|
||||
| Operator::Mod
|
||||
| Operator::FloorDiv => Self::MulDivRemain,
|
||||
// Addition, subtraction
|
||||
Operator::Add | Operator::Sub => Self::AddSub,
|
||||
// Bitwise shifts: <<, >>
|
||||
Operator::LShift | Operator::RShift => Self::LeftRightShift,
|
||||
// Bitwise operations: &, ^, |
|
||||
Operator::BitAnd => Self::BitAnd,
|
||||
Operator::BitXor | Operator::BitOr => Self::BitXorOr,
|
||||
// Exponentiation **
|
||||
Operator::Pow => Self::Exponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BoolOp> for OperatorPrecedence {
|
||||
fn from(operator: BoolOp) -> Self {
|
||||
match operator {
|
||||
BoolOp::And => Self::And,
|
||||
BoolOp::Or => Self::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ use std::fmt;
|
||||
|
||||
/// Representation of a Python version.
|
||||
///
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
/// N.B. This does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
@@ -41,8 +40,7 @@ impl PythonVersion {
|
||||
PythonVersion::PY312,
|
||||
PythonVersion::PY313,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn free_threaded_build_available(self) -> bool {
|
||||
@@ -84,7 +82,7 @@ impl fmt::Display for PythonVersion {
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use crate::PythonVersion;
|
||||
use super::PythonVersion;
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
@@ -11,12 +11,13 @@ use red_knot_python_semantic::lint::LintRegistry;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::{
|
||||
default_lint_registry, lint::RuleSelection, Db as SemanticDb, Program, ProgramSettings,
|
||||
PythonPlatform, PythonVersion, SearchPathSettings,
|
||||
PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_parser::{parse_unchecked, Mode};
|
||||
|
||||
/// Database that can be used for testing.
|
||||
|
||||
@@ -27,4 +27,4 @@ a persistent datastore based on [Workers KV](https://developers.cloudflare.com/w
|
||||
and exposed via a [Cloudflare Worker](https://developers.cloudflare.com/workers/learning/how-workers-works/).
|
||||
|
||||
The playground design is originally based on [Tailwind Play](https://play.tailwindcss.com/), with
|
||||
additional inspiration from the [Rome Tools Playground](https://docs.rome.tools/playground/).
|
||||
additional inspiration from the [Biome Playground](https://biomejs.dev/playground/).
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -3947,6 +3947,7 @@
|
||||
"RUF040",
|
||||
"RUF041",
|
||||
"RUF043",
|
||||
"RUF045",
|
||||
"RUF046",
|
||||
"RUF047",
|
||||
"RUF048",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user