Compare commits

...

21 Commits

Author SHA1 Message Date
Micha Reiser
c13a1814f7 Shrink TypeInference struct by 6 words 2025-02-16 15:40:50 +01:00
Alex Waygood
93aff36147 [red-knot] Improve handling of inherited class attributes (#16160) 2025-02-15 18:22:35 +00:00
Ayush Baweja
df45a9db64 [flake8-comprehensions]: Handle trailing comma in C403 fix (#16110)
## Summary

Resolves [#16099 ](https://github.com/astral-sh/ruff/issues/16099) based
on [#15929 ](https://github.com/astral-sh/ruff/pull/15929)

## Test Plan

Added test case `s = set([x for x in range(3)],)` and updated snapshot.

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-02-15 11:45:41 -06:00
InSync
3c69b685ee [ruff] Implicit class variable in dataclass (RUF045) (#14349)
## Summary

Implement lint rule to flag un-annotated variable assignments in dataclass definitions.

Resolves #12877.

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-02-15 09:08:13 -06:00
github-actions[bot]
171facd960 Sync vendored typeshed stubs (#16173)
Close and reopen this PR to trigger CI

Co-authored-by: typeshedbot <>
2025-02-15 10:01:34 +00:00
InSync
977447f9b8 Sort linters alphabetically (#16168)
## Summary

Resolves #16164.

Linters are now sorted by their names case-insensitively.

## Test Plan


![](https://github.com/user-attachments/assets/87ffd4d8-1ba5-4a4b-8fed-dd21a020bd27)

Also unit tests.
2025-02-14 22:05:08 +01:00
Micha Reiser
b3e99b25bf Fix missing serde feature for red_knot_python_semantic (#16169)
## Summary

Running `cargo test -p red_knot_python_semantic` failed because of a
missing serde feature. This PR enables the `ruff_python_ast`'`s `serde`
if the crate's `serde` feature is enabled

## Test Plan

`cargo test -p red_knot_python_semantic` compiles again
2025-02-14 20:31:55 +00:00
Carl Meyer
dcabb948f3 [red-knot] add special case for float/complex (#16166)
When adjusting the existing tests, I aimed to avoid dealing with the
special case in other tests if it's not necessary to do so (that is,
avoid using `float` and `complex` as examples where we just need "some
type"), and keep the tests for the special case mostly collected in the
mdtest dedicated to that purpose.

Fixes https://github.com/astral-sh/ruff/issues/14932
2025-02-14 12:24:10 -08:00
Vlad Nedelcu
219712860c [refurb] Check for subclasses includes subscript expressions (FURB189) (#16155)
## Summary

Added checks for subscript expressions on builtin classes as in FURB189.
The object is changed to use the collections objects and the types from
the subscript are kept.

Resolves #16130 

> Note: Added some comments in the code explaining why
## Test Plan


- Added a subscript dict and list class to the test file.
- Tested locally to check that the symbols are changed and the types are
kept.
- No modifications changed on optional `str` values.
2025-02-14 20:21:26 +01:00
Brent Westbrook
f58a54f043 Move red_knot_python_semantic::PythonVersion to the ruff_python_ast crate (#16147)
## Summary

This PR moves the `PythonVersion` struct from the
`red_knot_python_semantic` crate to the `ruff_python_ast` crate so that
it can be used more easily in the syntax error detection work. Compared
to that [prototype](https://github.com/astral-sh/ruff/pull/16090/) these
changes reduce us from 2 `PythonVersion` structs to 1.

This does not unify any of the `PythonVersion` *enums*, but I hope to
make some progress on that in a follow-up.

## Test Plan

Existing tests, this should not change any external behavior.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-14 12:48:08 -05:00
Junhson Jean-Baptiste
fa28dc5ccf [internal] Move Linter OperatorPrecedence into ruff_python_ast crate (#16162)
## Summary

This change begins to resolve #16071 by moving the `OperatorPrecedence`
structs from the `ruff_python_linter` crate into `ruff_python_ast`. This
PR also implements `precedence()` methods on the `Expr` and `ExprRef`
enums.

## Test Plan

Since this change mainly shifts existing logic, I didn't add any
additional tests. Existing tests do pass.
2025-02-14 15:55:07 +01:00
Dhruv Manilawala
63dd68e0ed Refactor symbol lookup APIs to hide re-export implementation details (#16133)
## Summary

This PR refactors the symbol lookup APIs to better facilitate the
re-export implementation. Specifically,
* Add `module_type_symbol` which returns the `Symbol` that's a member of
`types.ModuleType`
* Rename `symbol` -> `symbol_impl`; add `symbol` which delegates to
`symbol_impl` with `RequireExplicitReExport::No`
* Update `global_symbol` to do `symbol_impl` -> fall back to
`module_type_symbol` and default to `RequireExplicitReExport::No`
* Add `imported_symbol` to do `symbol_impl` with
`RequireExplicitReExport` as `Yes` if the module is in a stub file else
`No`
* Update `known_module_symbol` to use `imported_symbol` with a fallback
to `module_type_symbol`
* Update `ModuleLiteralType::member` to use `imported_symbol` with a
custom fallback

We could potentially also update `symbol_from_declarations` and
`symbol_from_bindings` to avoid passing in the `RequireExplicitReExport`
as it would be always `No` if called directly. We could add
`symbol_from_declarations_impl` and `symbol_from_bindings_impl`.

Looking at the `_impl` functions, I think we should move all of these
symbol related logic into `symbol.rs` where `Symbol` is defined and the
`_impl` could be private while we expose the public APIs at the crate
level. This would also make the `RequireExplicitReExport` an
implementation detail and the caller doesn't need to worry about it.
2025-02-14 15:25:48 +05:30
Dhruv Manilawala
60b3ef2c98 [red-knot] Support re-export conventions for stub files (#16073)
This is an alternative implementation to #15848.

## Summary

This PR adds support for re-export conventions for imports for stub
files.

**How does this work?**
* Add a new flag on the `Import` and `ImportFrom` definitions to
indicate whether they're being exported or not
* Add a new enum to indicate whether the symbol lookup is happening
within the same file or is being queried from another file (e.g., an
import statement)
* When a `Symbol` is being queried, we'll skip the definitions that are
(a) coming from a stub file (b) external lookup and (c) check the
re-export flag on the definition

This implementation does not yet support `__all__` and `*` imports as
both are features that needs to be implemented independently.

closes: #14099
closes: #15476 

## Test Plan

Add test cases, update existing ones if required.
2025-02-14 15:17:51 +05:30
InSync
3d0a58eb60 [pyupgrade] Unwrap unary expressions correctly (UP018) (#15919)
## Summary

Resolves #15859.

The rule now adds parentheses if the original call wraps an unary
expression and is:

* The left-hand side of a binary expression where the operator is `**`.
* The caller of a call expression.
* The subscripted of a subscript expression.
* The object of an attribute access.

The fix will also be marked as unsafe if there are any comments in its
range.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-02-14 08:42:00 +01:00
InSync
1db8392a5a Check for backtick-quoted shortcut links in CI (#16114)
## Summary

Follow-up to #16035.

`check_docs_formatted.py` will now report backtick-quoted shortcut links
in rule documentation. It uses a regular expression to find them. Such a
link:

* Starts with `[`, followed by <code>\`</code>, then a "name" sequence
of at least one non-backtick non-newline character, followed by another
<code>\`</code>, then ends with `]`.
* Is not followed by either a `[` or a `(`.
* Is not placed within a code block.

If the name is a known Ruff option name, that link is not considered a
violation.

## Test Plan

Manual.
2025-02-14 08:37:46 +01:00
Micha Reiser
81e202ed52 Make CallBinding::callable_ty required (#16135)
## Summary

The `callable_ty` is always known except in some TODO code where we can
use a `TODO` type instead.

## Test Plan

`cargo test`
2025-02-14 08:15:24 +01:00
Vlad Nedelcu
63c67750b1 Replace dead link for rome tools playground (#16153)
## Summary

Rome Tools Playground was renamed to Biome Playground. The link was
replaced to the new website.

Resolves #16143


## Test Plan

- Checked the linked is accessible from the README
2025-02-14 12:27:14 +05:30
Shaygan Hooshyari
0a75a1d56b Replace is-macro with implementation in enums (#16144) 2025-02-13 22:49:00 +00:00
Shaygan Hooshyari
bb15c7653a Use ubuntu-24 to run benchmarks (#16145) 2025-02-13 22:05:51 +00:00
Vlad Nedelcu
cb8b23d609 [flake8-pyi] Avoid flagging custom-typevar-for-self on metaclass methods (PYI019) (#16141) 2025-02-13 18:44:11 +00:00
Alex Waygood
be49151a3d [red-knot] Remove a parameter from the symbol_by_id() query (#16138) 2025-02-13 13:33:40 +00:00
101 changed files with 5166 additions and 881 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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 }

View File

@@ -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()
);
}
}

View File

@@ -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,

View File

@@ -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`.
///

View File

@@ -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};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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
```

View File

@@ -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)

View File

@@ -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__`

View File

@@ -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:

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
```

View File

@@ -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: ...

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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: ...
```

View File

@@ -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))

View File

@@ -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

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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::*;

View File

@@ -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::*;

View File

@@ -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.

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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)]

View File

@@ -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>;

View File

@@ -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.

View File

@@ -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::*;

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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: ...

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -1 +1 @@
c193cd2a36839c8e6336f350397f51ce52fedd5e
cc8ca939c0477a49fcce0554fa1743bd5c656a11

View File

@@ -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":

View File

@@ -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 = ...,

View File

@@ -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]: ...

View File

@@ -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: ...

View File

@@ -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

View File

@@ -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]],

View File

@@ -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]

View File

@@ -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`

View File

@@ -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: ...

View File

@@ -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: ...

View File

@@ -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]: ...

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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: ...

View File

@@ -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"]

View File

@@ -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]

View File

@@ -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: ...

View File

@@ -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):

View File

@@ -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 }

View File

@@ -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()
);
}
}

View File

@@ -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]

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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)],)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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]

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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)"
);
}
}

View File

@@ -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());

View File

@@ -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()),

View File

@@ -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(),
);

View File

@@ -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)}

View File

@@ -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;
}

View File

@@ -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")

View File

@@ -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,
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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__{}_{}",

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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[...]`

View File

@@ -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} {{

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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.

View 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,
}
}
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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
View File

@@ -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