Compare commits
18 Commits
micha/sals
...
0.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0e26e6fc8 | ||
|
|
e9941cd714 | ||
|
|
43bf1a8907 | ||
|
|
fda8b1f884 | ||
|
|
2d3f557875 | ||
|
|
bd27bfab5d | ||
|
|
155d34bbb9 | ||
|
|
04c887c8fc | ||
|
|
af43bd4b0f | ||
|
|
614917769e | ||
|
|
8b23086eac | ||
|
|
948549fcdc | ||
|
|
e67f7f243d | ||
|
|
c617b2a48a | ||
|
|
1685d95ed2 | ||
|
|
575deb5d4d | ||
|
|
edce559431 | ||
|
|
62e358e929 |
9
.github/workflows/ci.yaml
vendored
9
.github/workflows/ci.yaml
vendored
@@ -32,6 +32,8 @@ jobs:
|
||||
# Flag that is raised when any code is changed
|
||||
# This is superset of the linter and formatter
|
||||
code: ${{ steps.changed.outputs.code_any_changed }}
|
||||
# Flag that is raised when any code that affects the fuzzer is changed
|
||||
fuzz: ${{ steps.changed.outputs.fuzz_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -79,6 +81,11 @@ jobs:
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
fuzz:
|
||||
- fuzz/Cargo.toml
|
||||
- fuzz/Cargo.lock
|
||||
- fuzz/fuzz_targets/**
|
||||
|
||||
code:
|
||||
- "**/*"
|
||||
- "!**/*.md"
|
||||
@@ -288,7 +295,7 @@ jobs:
|
||||
name: "cargo fuzz build"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582))
|
||||
- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734))
|
||||
- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679))
|
||||
- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611))
|
||||
- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631))
|
||||
- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770))
|
||||
- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671))
|
||||
- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692))
|
||||
- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726))
|
||||
- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683))
|
||||
- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748))
|
||||
- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730))
|
||||
- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596))
|
||||
- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741))
|
||||
- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729))
|
||||
- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749))
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Preview features
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2513,7 +2513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2732,7 +2732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -3047,7 +3047,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Any
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Any` is a way to name the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
x: Any = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Aliased to a different name
|
||||
|
||||
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any as RenamedAny
|
||||
|
||||
x: RenamedAny = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Shadowed class
|
||||
|
||||
If you define your own class named `Any`, using that in a type expression refers to your class, and
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any:
|
||||
pass
|
||||
|
||||
x: Any
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
|
||||
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
|
||||
# to that but not to our locally defined class.
|
||||
y: Any = "not an Any" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
The spec allows you to define subclasses of `Any`.
|
||||
|
||||
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
|
||||
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
|
||||
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
|
||||
assignable to `int`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any):
|
||||
pass
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
|
||||
x: Subclass = 1 # error: [invalid-assignment]
|
||||
# TODO: no diagnostic
|
||||
y: int = Subclass() # error: [invalid-assignment]
|
||||
|
||||
def f() -> Subclass:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Subclass
|
||||
```
|
||||
@@ -0,0 +1,219 @@
|
||||
# Length (`len()`)
|
||||
|
||||
## Literal and constructed iterables
|
||||
|
||||
### Strings and bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(len("no\rmal")) # revealed: Literal[6]
|
||||
reveal_type(len(r"aw stri\ng")) # revealed: Literal[10]
|
||||
reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
|
||||
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
|
||||
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
|
||||
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
r"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
b"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
```py
|
||||
reveal_type(len(())) # revealed: Literal[0]
|
||||
reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
|
||||
# TODO: Handle constructor calls
|
||||
reveal_type(len(tuple())) # revealed: int
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
reveal_type(len([])) # revealed: int
|
||||
reveal_type(len([1])) # revealed: int
|
||||
reveal_type(len([1, 2])) # revealed: int
|
||||
reveal_type(len([*{}, *dict()])) # revealed: int
|
||||
|
||||
reveal_type(len({})) # revealed: int
|
||||
reveal_type(len({**{}})) # revealed: int
|
||||
reveal_type(len({**{}, **{}})) # revealed: int
|
||||
|
||||
reveal_type(len({1})) # revealed: int
|
||||
reveal_type(len({1, 2})) # revealed: int
|
||||
reveal_type(len({*[], 2})) # revealed: int
|
||||
|
||||
reveal_type(len(list())) # revealed: int
|
||||
reveal_type(len(set())) # revealed: int
|
||||
reveal_type(len(dict())) # revealed: int
|
||||
reveal_type(len(frozenset())) # revealed: int
|
||||
```
|
||||
|
||||
## `__len__`
|
||||
|
||||
The returned value of `__len__` is implicitly and recursively converted to `int`.
|
||||
|
||||
### Literal integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Zero:
|
||||
def __len__(self) -> Literal[0]: ...
|
||||
|
||||
class ZeroOrOne:
|
||||
def __len__(self) -> Literal[0, 1]: ...
|
||||
|
||||
class ZeroOrTrue:
|
||||
def __len__(self) -> Literal[0, True]: ...
|
||||
|
||||
class OneOrFalse:
|
||||
def __len__(self) -> Literal[1] | Literal[False]: ...
|
||||
|
||||
class OneOrFoo:
|
||||
def __len__(self) -> Literal[1, "foo"]: ...
|
||||
|
||||
class ZeroOrStr:
|
||||
def __len__(self) -> Literal[0] | str: ...
|
||||
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(ZeroOrStr())) # revealed: int
|
||||
```
|
||||
|
||||
### Literal booleans
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class LiteralTrue:
|
||||
def __len__(self) -> Literal[True]: ...
|
||||
|
||||
class LiteralFalse:
|
||||
def __len__(self) -> Literal[False]: ...
|
||||
|
||||
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
|
||||
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
from typing import Literal
|
||||
|
||||
class SomeEnum(Enum):
|
||||
AUTO = auto()
|
||||
INT = 2
|
||||
STR = "4"
|
||||
TUPLE = (8, "16")
|
||||
INT_2 = 3_2
|
||||
|
||||
class Auto:
|
||||
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
|
||||
|
||||
class Int:
|
||||
def __len__(self) -> Literal[SomeEnum.INT]: ...
|
||||
|
||||
class Str:
|
||||
def __len__(self) -> Literal[SomeEnum.STR]: ...
|
||||
|
||||
class Tuple:
|
||||
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
|
||||
|
||||
class IntUnion:
|
||||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: Literal[2]
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Negative:
|
||||
def __len__(self) -> Literal[-1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(Negative())) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class SecondOptionalArgument:
|
||||
def __len__(self, v: int = 0) -> Literal[0]: ...
|
||||
|
||||
class SecondRequiredArgument:
|
||||
def __len__(self, v: int) -> Literal[1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen:
|
||||
pass
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# Syntax errors
|
||||
|
||||
Test cases to ensure that red knot does not panic if there are syntax errors in the source code.
|
||||
|
||||
## Keyword as identifiers
|
||||
|
||||
When keywords are used as identifiers, the parser recovers from this syntax error by emitting an
|
||||
error and including the text value of the keyword to create the `Identifier` node.
|
||||
|
||||
### Name expression
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
pass = 1
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
type pass = 1
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
def True(for):
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `pass` used when not defined"
|
||||
for while in pass:
|
||||
pass
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `in` used when not defined"
|
||||
while in:
|
||||
pass
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `match` used when not defined"
|
||||
match while:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `case` used when not defined"
|
||||
case in:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
### Attribute expression
|
||||
|
||||
```py
|
||||
# TODO: Check when support for attribute expressions is added
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
for x in foo.pass:
|
||||
pass
|
||||
```
|
||||
@@ -267,3 +267,42 @@ reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Unicode
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "é"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\u9E6C"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\U0010FFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Surrogates
|
||||
|
||||
```py
|
||||
(a, b) = "\uD800\uDFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -11,8 +11,13 @@ pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::ProgramSettings;
|
||||
|
||||
use anyhow::Context;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -108,4 +113,66 @@ pub(crate) mod tests {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestDbBuilder<'a> {
|
||||
/// Target Python version
|
||||
python_version: PythonVersion,
|
||||
/// Path to a custom typeshed directory
|
||||
custom_typeshed: Option<SystemPathBuf>,
|
||||
/// Path and content pairs for files that should be present
|
||||
files: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> TestDbBuilder<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
python_version: PythonVersion::default(),
|
||||
custom_typeshed: None,
|
||||
files: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self {
|
||||
self.python_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
|
||||
self.custom_typeshed = Some(SystemPathBuf::from(path));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
|
||||
self.files.push((path, content));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system().create_directory_all(&src_root)?;
|
||||
|
||||
db.write_files(self.files)
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: self.python_version,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
.context("Failed to configure Program settings")?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
TestDbBuilder::new().build().expect("valid TestDb setup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,31 +166,15 @@ impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
|
||||
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
db.write_files(files)?;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
use crate::db::tests::TestDbBuilder;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
|
||||
#[test]
|
||||
fn function_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([("/src/foo.py", "def test(): pass")])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "def test(): pass")
|
||||
.build()?;
|
||||
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
@@ -207,7 +191,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn class_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([("/src/foo.py", "class Test: pass")])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.build()?;
|
||||
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
@@ -224,10 +210,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn alias_ty() -> anyhow::Result<()> {
|
||||
let db = setup_db([
|
||||
("/src/foo.py", "class Test: pass"),
|
||||
("/src/bar.py", "from foo import Test"),
|
||||
])?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.with_file("/src/bar.py", "from foo import Test")
|
||||
.build()?;
|
||||
|
||||
let bar = system_path_to_file(&db, "/src/bar.py").unwrap();
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ impl<'db> Symbol<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::tests::setup_db;
|
||||
use crate::db::tests::setup_db;
|
||||
|
||||
#[test]
|
||||
fn test_symbol_or_fall_back_to() {
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
|
||||
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, Module, Program};
|
||||
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
|
||||
|
||||
mod builder;
|
||||
mod diagnostic;
|
||||
@@ -302,13 +302,7 @@ fn declarations_ty<'db>(
|
||||
let declared_ty = if let Some(second) = all_types.next() {
|
||||
let mut builder = UnionBuilder::new(db).add(first);
|
||||
for other in [second].into_iter().chain(all_types) {
|
||||
// Make sure not to emit spurious errors relating to `Type::Todo`,
|
||||
// since we only infer this type due to a limitation in our current model.
|
||||
//
|
||||
// `Unknown` is different here, since we might infer `Unknown`
|
||||
// for one of these due to a variable being defined in one possible
|
||||
// control-flow branch but not another one.
|
||||
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
|
||||
if !first.is_equivalent_to(db, other) {
|
||||
conflicting.push(other);
|
||||
}
|
||||
builder = builder.add(other);
|
||||
@@ -600,11 +594,16 @@ impl<'db> Type<'db> {
|
||||
|
||||
/// Return true if this type is a [subtype of] type `target`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
///
|
||||
/// [subtype of]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
if !self.is_fully_static(db) || !target.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
match (self, target) {
|
||||
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
|
||||
@@ -764,8 +763,16 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is equivalent to type `other`.
|
||||
/// Return true if this type is [equivalent to] type `other`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
///
|
||||
/// [equivalent to]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
if !(self.is_fully_static(db) && other.is_fully_static(db)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO equivalent but not identical structural types, differently-ordered unions and
|
||||
// intersections, other cases?
|
||||
|
||||
@@ -776,7 +783,6 @@ impl<'db> Type<'db> {
|
||||
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
|
||||
// we understand `sys.version_info` branches.
|
||||
self == other
|
||||
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
|
||||
|| matches!((self, other),
|
||||
(
|
||||
Type::Instance(InstanceType { class: self_class }),
|
||||
@@ -790,6 +796,17 @@ impl<'db> Type<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if both `self` and `other` are the same gradual form
|
||||
/// (limited to `Any`, `Unknown`, or `Todo`).
|
||||
pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(Type::Unknown, Type::Unknown)
|
||||
| (Type::Any, Type::Any)
|
||||
| (Type::Todo(_), Type::Todo(_))
|
||||
)
|
||||
}
|
||||
|
||||
/// Return true if this type and `other` have no common elements.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return
|
||||
@@ -996,6 +1013,63 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the type does not contain any gradual forms (as a sub-part).
|
||||
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Any | Type::Unknown | Type::Todo(_) => false,
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::KnownInstance(_) => true,
|
||||
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
|
||||
// TODO: Ideally, we would iterate over the MRO of the class, check if all
|
||||
// bases are fully static, and only return `true` if that is the case.
|
||||
//
|
||||
// This does not work yet, because we currently infer `Unknown` for some
|
||||
// generic base classes that we don't understand yet. For example, `str`
|
||||
// is defined as `class str(Sequence[str])` in typeshed and we currently
|
||||
// compute its MRO as `(str, Unknown, object)`. This would make us think
|
||||
// that `str` is a gradual type, which causes all sorts of downstream
|
||||
// issues because it does not participate in equivalence/subtyping etc.
|
||||
//
|
||||
// Another problem is that we run into problems if we eagerly query the
|
||||
// MRO of class literals here. I have not fully investigated this, but
|
||||
// iterating over the MRO alone, without even acting on it, causes us to
|
||||
// infer `Unknown` for many classes.
|
||||
|
||||
true
|
||||
}
|
||||
Type::Union(union) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|elem| elem.is_fully_static(db)),
|
||||
Type::Intersection(intersection) => {
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.all(|elem| elem.is_fully_static(db))
|
||||
&& intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
.all(|elem| elem.is_fully_static(db))
|
||||
}
|
||||
Type::Tuple(tuple) => tuple
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|elem| elem.is_fully_static(db)),
|
||||
// TODO: Once we support them, make sure that we return `false` for other types
|
||||
// containing gradual forms such as `tuple[Any, ...]` or `Callable[..., str]`.
|
||||
// Conversely, make sure to return `true` for homogeneous tuples such as
|
||||
// `tuple[int, ...]`, once we add support for them.
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if there is just a single inhabitant for this type.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return `false`
|
||||
@@ -1343,21 +1417,76 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
||||
/// or `None` otherwise.
|
||||
///
|
||||
/// In the second case, the return type of `len()` in `typeshed` (`int`)
|
||||
/// is used as a fallback.
|
||||
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
|
||||
match ty {
|
||||
// TODO: Emit diagnostic for non-integers and negative integers
|
||||
Type::IntLiteral(value) => (value >= 0).then_some(ty),
|
||||
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
|
||||
Type::Union(union) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, *element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let usize_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(usize_len) = usize_len {
|
||||
return usize_len.try_into().ok().map(Type::IntLiteral);
|
||||
}
|
||||
|
||||
let return_ty = match self.call_dunder(db, "__len__", &[*self]) {
|
||||
// TODO: emit a diagnostic
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_ty(db)?
|
||||
}
|
||||
};
|
||||
|
||||
non_negative_int_literal(db, return_ty)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an object of this type.
|
||||
#[must_use]
|
||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
|
||||
match self {
|
||||
// TODO validate typed call arguments vs callable signature
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
if function_type.is_known(db, KnownFunction::RevealType) {
|
||||
CallOutcome::revealed(
|
||||
function_type.signature(db).return_ty,
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
)
|
||||
} else {
|
||||
CallOutcome::callable(function_type.signature(db).return_ty)
|
||||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => CallOutcome::revealed(
|
||||
function_type.signature(db).return_ty,
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
),
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
let normal_return_ty = function_type.signature(db).return_ty;
|
||||
|
||||
let [only_arg] = arg_types else {
|
||||
// TODO: Emit a diagnostic
|
||||
return CallOutcome::callable(normal_return_ty);
|
||||
};
|
||||
let len_ty = only_arg.len(db);
|
||||
|
||||
CallOutcome::callable(len_ty.unwrap_or(normal_return_ty))
|
||||
}
|
||||
}
|
||||
|
||||
_ => CallOutcome::callable(function_type.signature(db).return_ty),
|
||||
},
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
@@ -1560,6 +1689,7 @@ impl<'db> Type<'db> {
|
||||
Type::Never
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString,
|
||||
Type::KnownInstance(KnownInstanceType::Any) => Type::Any,
|
||||
_ => todo_type!(),
|
||||
}
|
||||
}
|
||||
@@ -1767,13 +1897,13 @@ impl<'db> KnownClass {
|
||||
}
|
||||
|
||||
pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
|
||||
core_module_symbol(db, self.canonical_module(), self.as_str())
|
||||
core_module_symbol(db, self.canonical_module(db), self.as_str())
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
/// Return the module in which we should look up the definition for this class
|
||||
pub(crate) const fn canonical_module(self) -> CoreStdlibModule {
|
||||
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
@@ -1791,10 +1921,18 @@ impl<'db> KnownClass {
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
|
||||
Self::NoneType => CoreStdlibModule::Typeshed,
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
|
||||
// TODO when we understand sys.version_info, we will need an explicit fallback here,
|
||||
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
|
||||
// singleton, but not for `typing._NoDefaultType`
|
||||
Self::NoDefaultType => CoreStdlibModule::TypingExtensions,
|
||||
Self::NoDefaultType => {
|
||||
let python_version = Program::get(db).target_version(db);
|
||||
|
||||
// typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
|
||||
// singleton, but not for `typing._NoDefaultType`. So we need to switch
|
||||
// to `typing._NoDefaultType` for newer versions:
|
||||
if python_version >= PythonVersion::PY313 {
|
||||
CoreStdlibModule::Typing
|
||||
} else {
|
||||
CoreStdlibModule::TypingExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1854,11 +1992,11 @@ impl<'db> KnownClass {
|
||||
};
|
||||
|
||||
let module = file_to_module(db, file)?;
|
||||
candidate.check_module(&module).then_some(candidate)
|
||||
candidate.check_module(db, &module).then_some(candidate)
|
||||
}
|
||||
|
||||
/// Return `true` if the module of `self` matches `module_name`
|
||||
fn check_module(self, module: &Module) -> bool {
|
||||
fn check_module(self, db: &'db dyn Db, module: &Module) -> bool {
|
||||
if !module.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
@@ -1878,7 +2016,7 @@ impl<'db> KnownClass {
|
||||
| Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::VersionInfo
|
||||
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
|
||||
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
|
||||
matches!(module.name().as_str(), "typing" | "typing_extensions")
|
||||
@@ -1902,6 +2040,8 @@ pub enum KnownInstanceType<'db> {
|
||||
NoReturn,
|
||||
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
|
||||
Never,
|
||||
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
|
||||
Any,
|
||||
/// A single instance of `typing.TypeVar`
|
||||
TypeVar(TypeVarInstance<'db>),
|
||||
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
||||
@@ -1919,6 +2059,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::TypeVar(_) => "TypeVar",
|
||||
Self::NoReturn => "NoReturn",
|
||||
Self::Never => "Never",
|
||||
Self::Any => "Any",
|
||||
Self::TypeAliasType(_) => "TypeAliasType",
|
||||
}
|
||||
}
|
||||
@@ -1933,6 +2074,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Union
|
||||
| Self::NoReturn
|
||||
| Self::Never
|
||||
| Self::Any
|
||||
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
@@ -1946,6 +2088,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Union => "typing.Union",
|
||||
Self::NoReturn => "typing.NoReturn",
|
||||
Self::Never => "typing.Never",
|
||||
Self::Any => "typing.Any",
|
||||
Self::TypeVar(typevar) => typevar.name(db),
|
||||
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
||||
}
|
||||
@@ -1960,6 +2103,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Union => KnownClass::SpecialForm,
|
||||
Self::NoReturn => KnownClass::SpecialForm,
|
||||
Self::Never => KnownClass::SpecialForm,
|
||||
Self::Any => KnownClass::Object,
|
||||
Self::TypeVar(_) => KnownClass::TypeVar,
|
||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
}
|
||||
@@ -1979,6 +2123,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
return None;
|
||||
}
|
||||
match (module.name().as_str(), instance_name) {
|
||||
("typing", "Any") => Some(Self::Any),
|
||||
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
|
||||
("typing" | "typing_extensions", "LiteralString") => Some(Self::LiteralString),
|
||||
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
|
||||
@@ -2515,13 +2660,15 @@ pub enum KnownFunction {
|
||||
ConstraintFunction(KnownConstraintFunction),
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
/// `builtins.len`
|
||||
Len,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
||||
match self {
|
||||
Self::ConstraintFunction(f) => Some(f),
|
||||
Self::RevealType => None,
|
||||
Self::RevealType | Self::Len => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2538,6 +2685,7 @@ impl KnownFunction {
|
||||
"issubclass" if definition.is_builtin_definition(db) => Some(
|
||||
KnownFunction::ConstraintFunction(KnownConstraintFunction::IsSubclass),
|
||||
),
|
||||
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -2647,7 +2795,11 @@ impl<'db> Class<'db> {
|
||||
pub fn is_subclass_of(self, db: &'db dyn Db, other: Class) -> bool {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
|
||||
self.iter_mro(db).contains(&ClassBase::Class(other))
|
||||
self.is_subclass_of_base(db, other)
|
||||
}
|
||||
|
||||
fn is_subclass_of_base(self, db: &'db dyn Db, other: impl Into<ClassBase<'db>>) -> bool {
|
||||
self.iter_mro(db).contains(&other.into())
|
||||
}
|
||||
|
||||
/// Return the explicit `metaclass` of this class, if one is defined.
|
||||
@@ -2988,8 +3140,9 @@ pub struct StringLiteralType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> StringLiteralType<'db> {
|
||||
pub fn len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).len()
|
||||
/// The length of the string, as would be returned by Python's `len()`.
|
||||
pub fn python_len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2999,6 +3152,12 @@ pub struct BytesLiteralType<'db> {
|
||||
value: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl<'db> BytesLiteralType<'db> {
|
||||
pub fn python_len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).len()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct SliceLiteralType<'db> {
|
||||
start: Option<i32>,
|
||||
@@ -3052,38 +3211,16 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::ProgramSettings;
|
||||
use crate::PythonVersion;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast as ast;
|
||||
use test_case::test_case;
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
/// given a db.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -3249,7 +3386,9 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Ty::BuiltinInstance("object"), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::Unknown, Ty::Unknown)]
|
||||
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::Any, Ty::Any)]
|
||||
#[test_case(Ty::Any, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Any)]
|
||||
@@ -3357,6 +3496,18 @@ pub(crate) mod tests {
|
||||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Any, Ty::Any)]
|
||||
#[test_case(Ty::Any, Ty::None)]
|
||||
#[test_case(Ty::Unknown, Ty::Unknown)]
|
||||
#[test_case(Ty::Todo, Ty::Todo)]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(0)]))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
|
||||
fn is_not_equivalent_to(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(!from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never, Ty::Never)]
|
||||
#[test_case(Ty::Never, Ty::None)]
|
||||
#[test_case(Ty::Never, Ty::BuiltinInstance("int"))]
|
||||
@@ -3540,13 +3691,28 @@ pub(crate) mod tests {
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::BooleanLiteral(false))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))]
|
||||
fn is_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
|
||||
/// the fallback to `typing_extensions` is working correctly.
|
||||
/// See [`KnownClass::canonical_module`] for more information.
|
||||
#[test_case(PythonVersion::PY312)]
|
||||
#[test_case(PythonVersion::PY313)]
|
||||
fn no_default_type_is_singleton(python_version: PythonVersion) {
|
||||
let db = TestDbBuilder::new()
|
||||
.with_python_version(python_version)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db);
|
||||
|
||||
assert!(no_default.is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::IntLiteral(1))]
|
||||
@@ -3585,6 +3751,41 @@ pub(crate) mod tests {
|
||||
assert!(!from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never)]
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::StringLiteral("abc"))]
|
||||
#[test_case(Ty::LiteralString)]
|
||||
#[test_case(Ty::BytesLiteral("abc"))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Str))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Object))]
|
||||
#[test_case(Ty::KnownClassInstance(KnownClass::Type))]
|
||||
#[test_case(Ty::BuiltinClassLiteral("str"))]
|
||||
#[test_case(Ty::TypingLiteral)]
|
||||
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::None]))]
|
||||
#[test_case(Ty::Intersection{pos: vec![Ty::KnownClassInstance(KnownClass::Str)], neg: vec![Ty::LiteralString]})]
|
||||
#[test_case(Ty::Tuple(vec![]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::KnownClassInstance(KnownClass::Object)]))]
|
||||
fn is_fully_static(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_fully_static(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Any)]
|
||||
#[test_case(Ty::Unknown)]
|
||||
#[test_case(Ty::Todo)]
|
||||
#[test_case(Ty::Union(vec![Ty::Any, Ty::KnownClassInstance(KnownClass::Str)]))]
|
||||
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::Unknown]))]
|
||||
#[test_case(Ty::Intersection{pos: vec![Ty::Any], neg: vec![Ty::LiteralString]})]
|
||||
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::Any]))]
|
||||
fn is_not_fully_static(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(!from.into_type(&db).is_fully_static(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
|
||||
#[test_case(Ty::IntLiteral(-1))]
|
||||
#[test_case(Ty::StringLiteral("foo"))]
|
||||
@@ -3639,7 +3840,10 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn typing_vs_typeshed_no_default() {
|
||||
let db = setup_db();
|
||||
let db = TestDbBuilder::new()
|
||||
.with_python_version(PythonVersion::PY313)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let typing_no_default = typing_symbol(&db, "NoDefault").expect_type();
|
||||
let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault").expect_type();
|
||||
@@ -3759,19 +3963,6 @@ pub(crate) mod tests {
|
||||
let todo3 = todo_type!();
|
||||
let todo4 = todo_type!();
|
||||
|
||||
assert!(todo1.is_equivalent_to(&db, todo2));
|
||||
assert!(todo3.is_equivalent_to(&db, todo4));
|
||||
assert!(todo1.is_equivalent_to(&db, todo3));
|
||||
|
||||
assert!(todo1.is_subtype_of(&db, todo2));
|
||||
assert!(todo2.is_subtype_of(&db, todo1));
|
||||
|
||||
assert!(todo3.is_subtype_of(&db, todo4));
|
||||
assert!(todo4.is_subtype_of(&db, todo3));
|
||||
|
||||
assert!(todo1.is_subtype_of(&db, todo3));
|
||||
assert!(todo3.is_subtype_of(&db, todo1));
|
||||
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
assert!(int.is_assignable_to(&db, todo1));
|
||||
|
||||
@@ -73,7 +73,8 @@ impl<'db> UnionBuilder<'db> {
|
||||
// supertype of bool. Therefore, we are done.
|
||||
break;
|
||||
}
|
||||
if ty.is_subtype_of(self.db, *element) {
|
||||
|
||||
if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) {
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
@@ -259,7 +260,9 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive) {
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_same_gradual_form(new_positive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// same rule, reverse order
|
||||
@@ -375,36 +378,14 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::stdlib::typing_symbol;
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use test_case::test_case;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union() {
|
||||
let db = setup_db();
|
||||
@@ -497,6 +478,17 @@ mod tests {
|
||||
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
|
||||
let u = UnionType::from_elements(&db, [t0, t1, t1]);
|
||||
|
||||
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
@@ -604,6 +596,42 @@ mod tests {
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(
|
||||
ty,
|
||||
IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
@@ -626,59 +654,85 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn intersection_negation_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||
let ht = typing_symbol(&db, "Hashable")
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
// sh_t: Sized & Hashable
|
||||
let sh_t = IntersectionBuilder::new(&db)
|
||||
.add_positive(st)
|
||||
.add_positive(ht)
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
|
||||
// intersection: A & B
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(sh_t.pos_vec(&db), &[st, ht]);
|
||||
assert_eq!(sh_t.neg_vec(&db), &[]);
|
||||
assert_eq!(intersection.pos_vec(&db), &[a, b]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
|
||||
// ~sh_t => ~Sized | ~Hashable
|
||||
let not_s_h_t = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Intersection(sh_t))
|
||||
// ~intersection => ~A | ~B
|
||||
let negated_intersection = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Intersection(intersection))
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
// should have as elements: (~Sized),(~Hashable)
|
||||
let not_st = st.negate(&db);
|
||||
let not_ht = ht.negate(&db);
|
||||
assert_eq!(not_s_h_t.elements(&db), &[not_st, not_ht]);
|
||||
// should have as elements ~A and ~B
|
||||
let not_a = a.negate(&db);
|
||||
let not_b = b.negate(&db);
|
||||
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_intersection_negation_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let it = KnownClass::Int.to_instance(&db);
|
||||
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||
let ht = typing_symbol(&db, "Hashable")
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
// s_not_h_t: Sized & ~Hashable
|
||||
let s_not_h_t = IntersectionBuilder::new(&db)
|
||||
.add_positive(st)
|
||||
.add_negative(ht)
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
// a_not_b: A & ~B
|
||||
let a_not_b = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_negative(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(s_not_h_t.pos_vec(&db), &[st]);
|
||||
assert_eq!(s_not_h_t.neg_vec(&db), &[ht]);
|
||||
assert_eq!(a_not_b.pos_vec(&db), &[a]);
|
||||
assert_eq!(a_not_b.neg_vec(&db), &[b]);
|
||||
|
||||
// let's build int & ~(Sized & ~Hashable)
|
||||
let tt = IntersectionBuilder::new(&db)
|
||||
.add_positive(it)
|
||||
.add_negative(Type::Intersection(s_not_h_t))
|
||||
// let's build
|
||||
// int & ~(A & ~B)
|
||||
// = int & ~(A & ~B)
|
||||
// = int & (~A | B)
|
||||
// = (int & ~A) | (int & B)
|
||||
let t = IntersectionBuilder::new(&db)
|
||||
.add_positive(int)
|
||||
.add_negative(Type::Intersection(a_not_b))
|
||||
.build();
|
||||
|
||||
// int & ~(Sized & ~Hashable)
|
||||
// -> int & (~Sized | Hashable)
|
||||
// -> (int & ~Sized) | (int & Hashable)
|
||||
assert_eq!(tt.display(&db).to_string(), "int & ~Sized | int & Hashable");
|
||||
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -357,31 +357,10 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
|
||||
|
||||
@@ -1660,7 +1660,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let value_ty = self.expression_ty(value);
|
||||
let name_ast_id = name.scoped_expression_id(self.db, self.scope());
|
||||
|
||||
let target_ty = match assignment.target() {
|
||||
let mut target_ty = match assignment.target() {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db, unpack);
|
||||
// Only copy the diagnostics if this is the first assignment to avoid duplicating the
|
||||
@@ -1674,6 +1674,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
TargetKind::Name => value_ty,
|
||||
};
|
||||
|
||||
if let Some(known_instance) = file_to_module(self.db, definition.file(self.db))
|
||||
.as_ref()
|
||||
.and_then(|module| KnownInstanceType::try_from_module_and_symbol(module, &name.id))
|
||||
{
|
||||
target_ty = Type::KnownInstance(known_instance);
|
||||
}
|
||||
|
||||
self.store_expression_type(name, target_ty);
|
||||
self.add_binding(name.into(), definition, target_ty);
|
||||
}
|
||||
@@ -1893,12 +1900,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
is_async: _,
|
||||
} = for_statement;
|
||||
|
||||
self.infer_standalone_expression(iter);
|
||||
|
||||
// TODO more complex assignment targets
|
||||
if let ast::Expr::Name(name) = &**target {
|
||||
self.infer_definition(name);
|
||||
} else {
|
||||
self.infer_standalone_expression(iter);
|
||||
self.infer_expression(target);
|
||||
}
|
||||
self.infer_body(body);
|
||||
@@ -4653,6 +4659,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
KnownInstanceType::Any => Type::Any,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5038,68 +5045,19 @@ fn perform_membership_test_comparison<'db>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::FileScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::types::check_types;
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
use crate::{HasTy, SemanticModel};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
fn setup_db_with_custom_typeshed<'a>(
|
||||
typeshed: &str,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
|
||||
db.write_files(files)
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
custom_typeshed: Some(SystemPathBuf::from(typeshed)),
|
||||
..SearchPathSettings::new(src_root)
|
||||
},
|
||||
},
|
||||
)
|
||||
.context("Failed to create Program")?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
|
||||
let file = system_path_to_file(db, file_name).expect("file to exist");
|
||||
@@ -5494,17 +5452,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> {
|
||||
let db = setup_db_with_custom_typeshed(
|
||||
"/typeshed",
|
||||
[
|
||||
("/src/a.py", "c = copyright"),
|
||||
(
|
||||
"/typeshed/stdlib/builtins.pyi",
|
||||
"def copyright() -> None: ...",
|
||||
),
|
||||
("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"),
|
||||
],
|
||||
)?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_custom_typeshed("/typeshed")
|
||||
.with_file("/src/a.py", "c = copyright")
|
||||
.with_file(
|
||||
"/typeshed/stdlib/builtins.pyi",
|
||||
"def copyright() -> None: ...",
|
||||
)
|
||||
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-")
|
||||
.build()?;
|
||||
|
||||
assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]");
|
||||
|
||||
@@ -5513,14 +5469,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unknown_builtin_later_defined() -> anyhow::Result<()> {
|
||||
let db = setup_db_with_custom_typeshed(
|
||||
"/typeshed",
|
||||
[
|
||||
("/src/a.py", "x = foo"),
|
||||
("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1"),
|
||||
("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"),
|
||||
],
|
||||
)?;
|
||||
let db = TestDbBuilder::new()
|
||||
.with_custom_typeshed("/typeshed")
|
||||
.with_file("/src/a.py", "x = foo")
|
||||
.with_file("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1")
|
||||
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-")
|
||||
.build()?;
|
||||
|
||||
assert_public_ty(&db, "/src/a.py", "x", "Unknown");
|
||||
|
||||
|
||||
@@ -379,6 +379,7 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -406,6 +407,12 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
fn from(value: Class<'db>) -> Self {
|
||||
ClassBase::Class(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use super::tests::{setup_db, Ty};
|
||||
use crate::db::tests::TestDb;
|
||||
use super::tests::Ty;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::KnownClass;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
|
||||
@@ -213,6 +213,12 @@ mod stable {
|
||||
singleton_implies_single_valued, db,
|
||||
forall types t. t.is_singleton(db) => t.is_single_valued(db)
|
||||
);
|
||||
|
||||
// If `T` contains a gradual form, it should not participate in subtyping
|
||||
type_property_test!(
|
||||
non_fully_static_types_do_not_participate_in_subtyping, db,
|
||||
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
|
||||
@@ -189,32 +189,9 @@ impl<'db> Parameter<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, FunctionType};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
#[track_caller]
|
||||
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {
|
||||
|
||||
@@ -95,7 +95,8 @@ impl<'db> Unpacker<'db> {
|
||||
// there would be a cost and it's not clear that it's worth it.
|
||||
let value_ty = Type::tuple(
|
||||
self.db,
|
||||
std::iter::repeat(Type::LiteralString).take(string_literal_ty.len(self.db)),
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db)),
|
||||
);
|
||||
self.unpack(target, value_ty, scope);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from airflow.operators import PythonOperator
|
||||
from airflow.providers.airbyte.operators.airbyte import AirbyteTriggerSyncOperator
|
||||
from airflow.providers.amazon.aws.operators.appflow import AppflowFlowRunOperator
|
||||
|
||||
|
||||
def my_callable():
|
||||
@@ -6,11 +8,15 @@ def my_callable():
|
||||
|
||||
|
||||
my_task = PythonOperator(task_id="my_task", callable=my_callable)
|
||||
my_task_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
|
||||
incorrect_name = PythonOperator(task_id="my_task") # AIR001
|
||||
|
||||
incorrect_name = PythonOperator(task_id="my_task")
|
||||
incorrect_name_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
|
||||
my_task = AirbyteTriggerSyncOperator(task_id="my_task", callable=my_callable)
|
||||
incorrect_name = AirbyteTriggerSyncOperator(task_id="my_task") # AIR001
|
||||
|
||||
from my_module import MyClass
|
||||
my_task = AppflowFlowRunOperator(task_id="my_task", callable=my_callable)
|
||||
incorrect_name = AppflowFlowRunOperator(task_id="my_task") # AIR001
|
||||
|
||||
incorrect_name = MyClass(task_id="my_task")
|
||||
# Consider only from the `airflow.operators` (or providers operators) module
|
||||
from airflow import MyOperator
|
||||
|
||||
incorrect_name = MyOperator(task_id="my_task")
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
from airflow.triggers.external_task import TaskStateTrigger
|
||||
from airflow.www.auth import has_access
|
||||
from airflow.api_connexion.security import requires_access
|
||||
from airflow.metrics.validators import AllowListValidator
|
||||
from airflow.metrics.validators import BlockListValidator
|
||||
from airflow.utils import dates
|
||||
from airflow.utils.dates import date_range, datetime_to_nano, days_ago
|
||||
from airflow.utils.dates import (
|
||||
date_range,
|
||||
datetime_to_nano,
|
||||
days_ago,
|
||||
infer_time_unit,
|
||||
parse_execution_date,
|
||||
round_time,
|
||||
scale_time_units,
|
||||
)
|
||||
from airflow.utils.file import TemporaryDirectory, mkdirs
|
||||
from airflow.utils.state import SHUTDOWN, terminating_states
|
||||
from airflow.utils.dag_cycle_tester import test_cycle
|
||||
|
||||
date_range
|
||||
days_ago
|
||||
|
||||
TaskStateTrigger
|
||||
|
||||
|
||||
has_access
|
||||
requires_access
|
||||
|
||||
AllowListValidator
|
||||
BlockListValidator
|
||||
|
||||
dates.date_range
|
||||
dates.days_ago
|
||||
|
||||
date_range
|
||||
days_ago
|
||||
parse_execution_date
|
||||
round_time
|
||||
scale_time_units
|
||||
infer_time_unit
|
||||
|
||||
|
||||
# This one was not deprecated.
|
||||
datetime_to_nano
|
||||
dates.datetime_to_nano
|
||||
|
||||
TemporaryDirectory
|
||||
mkdirs
|
||||
|
||||
SHUTDOWN
|
||||
terminating_states
|
||||
|
||||
|
||||
test_cycle
|
||||
|
||||
@@ -57,3 +57,12 @@ value = f"{rf"\{1}"}"
|
||||
f"{{}}+-\d"
|
||||
f"\n{{}}+-\d+"
|
||||
f"\n{{}}<EFBFBD>+-\d+"
|
||||
|
||||
# See https://github.com/astral-sh/ruff/issues/11491
|
||||
total = 10
|
||||
ok = 7
|
||||
incomplete = 3
|
||||
s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
|
||||
|
||||
# Debug text (should trigger)
|
||||
t = f"{'\InHere'=}"
|
||||
|
||||
50
crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py
vendored
Normal file
50
crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import math
|
||||
|
||||
|
||||
### Safely fixable
|
||||
|
||||
# Arguments are not checked
|
||||
int(id())
|
||||
int(len([]))
|
||||
int(ord(foo))
|
||||
int(hash(foo, bar))
|
||||
int(int(''))
|
||||
|
||||
int(math.comb())
|
||||
int(math.factorial())
|
||||
int(math.gcd())
|
||||
int(math.lcm())
|
||||
int(math.isqrt())
|
||||
int(math.perm())
|
||||
|
||||
|
||||
### Unsafe
|
||||
|
||||
int(math.ceil())
|
||||
int(math.floor())
|
||||
int(math.trunc())
|
||||
|
||||
|
||||
### `round()`
|
||||
|
||||
## Errors
|
||||
int(round(0))
|
||||
int(round(0, 0))
|
||||
int(round(0, None))
|
||||
|
||||
int(round(0.1))
|
||||
int(round(0.1, None))
|
||||
|
||||
# Argument type is not checked
|
||||
foo = type("Foo", (), {"__round__": lambda self: 4.2})()
|
||||
|
||||
int(round(foo))
|
||||
int(round(foo, 0))
|
||||
int(round(foo, None))
|
||||
|
||||
## No errors
|
||||
int(round(0, 3.14))
|
||||
int(round(0, non_literal))
|
||||
int(round(0, 0), base)
|
||||
int(round(0, 0, extra=keyword))
|
||||
int(round(0.1, 0))
|
||||
@@ -74,3 +74,21 @@ re.sub(
|
||||
"",
|
||||
s, # string
|
||||
)
|
||||
|
||||
# A diagnostic should not be emitted for `sub` replacements with backreferences or
|
||||
# most other ASCII escapes
|
||||
re.sub(r"a", r"\g<0>\g<0>\g<0>", "a")
|
||||
re.sub(r"a", r"\1", "a")
|
||||
re.sub(r"a", r"\s", "a")
|
||||
|
||||
# Escapes like \n are "processed":
|
||||
# `re.sub(r"a", r"\n", some_string)` is fixed to `some_string.replace("a", "\n")`
|
||||
# *not* `some_string.replace("a", "\\n")`.
|
||||
# We currently emit diagnostics for some of these without fixing them.
|
||||
re.sub(r"a", "\n", "a")
|
||||
re.sub(r"a", r"\n", "a")
|
||||
re.sub(r"a", "\a", "a")
|
||||
re.sub(r"a", r"\a", "a")
|
||||
|
||||
re.sub(r"a", "\?", "a")
|
||||
re.sub(r"a", r"\?", "a")
|
||||
17
crates/ruff_linter/resources/test/fixtures/ruff/RUF055_1.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/ruff/RUF055_1.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Test that RUF055 can follow a single str assignment for both the pattern and
|
||||
the replacement argument to re.sub
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
pat1 = "needle"
|
||||
|
||||
re.sub(pat1, "", haystack)
|
||||
|
||||
# aliases are not followed, so this one should not trigger the rule
|
||||
if pat4 := pat1:
|
||||
re.sub(pat4, "", haystack)
|
||||
|
||||
# also works for the `repl` argument in sub
|
||||
repl = "new"
|
||||
re.sub(r"abc", repl, haystack)
|
||||
@@ -1093,6 +1093,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryCastToInt) {
|
||||
ruff::rules::unnecessary_cast_to_int(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
|
||||
@@ -1554,11 +1554,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
.rules
|
||||
.enabled(Rule::AirflowVariableNameTaskIdMismatch)
|
||||
{
|
||||
if let Some(diagnostic) =
|
||||
airflow::rules::variable_name_task_id(checker, targets, value)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
airflow::rules::variable_name_task_id(checker, targets, value);
|
||||
}
|
||||
if checker.settings.rules.enabled(Rule::SelfAssigningVariable) {
|
||||
pylint::rules::self_assignment(checker, assign);
|
||||
|
||||
@@ -983,6 +983,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
|
||||
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
|
||||
(Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral),
|
||||
(Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt),
|
||||
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Violation for Airflow3Removal {
|
||||
match replacement {
|
||||
Replacement::None => format!("`{deprecated}` is removed in Airflow 3.0"),
|
||||
Replacement::Name(name) => {
|
||||
format!("`{deprecated}` is removed in Airflow 3.0; use {name} instead")
|
||||
format!("`{deprecated}` is removed in Airflow 3.0; use `{name}` instead")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,13 +103,75 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
|
||||
.semantic()
|
||||
.resolve_qualified_name(expr)
|
||||
.and_then(|qualname| match qualname.segments() {
|
||||
["airflow", "utils", "dates", "date_range"] => {
|
||||
["airflow", "triggers", "external_task", "TaskStateTrigger"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "www", "auth", "has_access"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.www.auth.has_access_*".to_string()),
|
||||
)),
|
||||
["airflow", "api_connexion", "security", "requires_access"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.api_connexion.security.requires_access_*".to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.metrics.validators
|
||||
["airflow", "metrics", "validators", "AllowListValidator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.metrics.validators.PatternAllowListValidator".to_string(),
|
||||
),
|
||||
)),
|
||||
["airflow", "metrics", "validators", "BlockListValidator"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name(
|
||||
"airflow.metrics.validators.PatternBlockListValidator".to_string(),
|
||||
),
|
||||
)),
|
||||
// airflow.utils.dates
|
||||
["airflow", "utils", "dates", "date_range"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("airflow.timetables.".to_string()),
|
||||
)),
|
||||
["airflow", "utils", "dates", "days_ago"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("datetime.timedelta()".to_string()),
|
||||
Replacement::Name("pendulum.today('UTC').add(days=-N, ...)".to_string()),
|
||||
)),
|
||||
["airflow", "utils", "dates", "parse_execution_date"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "dates", "round_time"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "dates", "scale_time_units"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "dates", "infer_time_unit"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
// airflow.utils.file
|
||||
["airflow", "utils", "file", "TemporaryDirectory"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "file", "mkdirs"] => Some((
|
||||
qualname.to_string(),
|
||||
Replacement::Name("pendulum.today('UTC').add(days=-N, ...)".to_string()),
|
||||
)),
|
||||
// airflow.utils.state
|
||||
["airflow", "utils", "state", "SHUTDOWN"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "state", "terminating_states"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
// airflow.uilts
|
||||
["airflow", "utils", "dag_cycle_tester", "test_cycle"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
["airflow", "utils", "decorators", "apply_defaults"] => {
|
||||
Some((qualname.to_string(), Replacement::None))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some((deprecated, replacement)) = result {
|
||||
|
||||
@@ -45,21 +45,17 @@ impl Violation for AirflowVariableNameTaskIdMismatch {
|
||||
}
|
||||
|
||||
/// AIR001
|
||||
pub(crate) fn variable_name_task_id(
|
||||
checker: &mut Checker,
|
||||
targets: &[Expr],
|
||||
value: &Expr,
|
||||
) -> Option<Diagnostic> {
|
||||
pub(crate) fn variable_name_task_id(checker: &mut Checker, targets: &[Expr], value: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have more than one target, we can't do anything.
|
||||
let [target] = targets else {
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target else {
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
|
||||
// If the value is not a call, we can't do anything.
|
||||
@@ -67,33 +63,58 @@ pub(crate) fn variable_name_task_id(
|
||||
func, arguments, ..
|
||||
}) = value
|
||||
else {
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
|
||||
// If the function doesn't come from Airflow, we can't do anything.
|
||||
// If the function doesn't come from Airflow's operators module (builtin or providers), we
|
||||
// can't do anything.
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["airflow", ..]))
|
||||
.is_some_and(|qualified_name| {
|
||||
match qualified_name.segments() {
|
||||
// Match `airflow.operators.*`
|
||||
["airflow", "operators", ..] => true,
|
||||
|
||||
// Match `airflow.providers.**.operators.*`
|
||||
["airflow", "providers", rest @ ..] => {
|
||||
// Ensure 'operators' exists somewhere in the middle
|
||||
if let Some(pos) = rest.iter().position(|&s| s == "operators") {
|
||||
pos + 1 < rest.len() // Check that 'operators' is not the last element
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
{
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the call doesn't have a `task_id` keyword argument, we can't do anything.
|
||||
let keyword = arguments.find_keyword("task_id")?;
|
||||
let Some(keyword) = arguments.find_keyword("task_id") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the keyword argument is not a string, we can't do anything.
|
||||
let ast::ExprStringLiteral { value: task_id, .. } = keyword.value.as_string_literal_expr()?;
|
||||
let Some(ast::ExprStringLiteral { value: task_id, .. }) =
|
||||
keyword.value.as_string_literal_expr()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the target name is the same as the task_id, no violation.
|
||||
if task_id == id.as_str() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
|
||||
Some(Diagnostic::new(
|
||||
let diagnostic = Diagnostic::new(
|
||||
AirflowVariableNameTaskIdMismatch {
|
||||
task_id: task_id.to_string(),
|
||||
},
|
||||
target.range(),
|
||||
))
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR001.py:11:1: AIR001 Task variable name should match the `task_id`: "my_task"
|
||||
|
|
||||
9 | my_task_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
|
||||
10 |
|
||||
11 | incorrect_name = PythonOperator(task_id="my_task")
|
||||
10 | my_task = PythonOperator(task_id="my_task", callable=my_callable)
|
||||
11 | incorrect_name = PythonOperator(task_id="my_task") # AIR001
|
||||
| ^^^^^^^^^^^^^^ AIR001
|
||||
12 | incorrect_name_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
|
||||
12 |
|
||||
13 | my_task = AirbyteTriggerSyncOperator(task_id="my_task", callable=my_callable)
|
||||
|
|
||||
|
||||
AIR001.py:12:1: AIR001 Task variable name should match the `task_id`: "my_task_2"
|
||||
AIR001.py:14:1: AIR001 Task variable name should match the `task_id`: "my_task"
|
||||
|
|
||||
11 | incorrect_name = PythonOperator(task_id="my_task")
|
||||
12 | incorrect_name_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
|
||||
| ^^^^^^^^^^^^^^^^ AIR001
|
||||
13 |
|
||||
14 | from my_module import MyClass
|
||||
13 | my_task = AirbyteTriggerSyncOperator(task_id="my_task", callable=my_callable)
|
||||
14 | incorrect_name = AirbyteTriggerSyncOperator(task_id="my_task") # AIR001
|
||||
| ^^^^^^^^^^^^^^ AIR001
|
||||
15 |
|
||||
16 | my_task = AppflowFlowRunOperator(task_id="my_task", callable=my_callable)
|
||||
|
|
||||
|
||||
AIR001.py:17:1: AIR001 Task variable name should match the `task_id`: "my_task"
|
||||
|
|
||||
16 | my_task = AppflowFlowRunOperator(task_id="my_task", callable=my_callable)
|
||||
17 | incorrect_name = AppflowFlowRunOperator(task_id="my_task") # AIR001
|
||||
| ^^^^^^^^^^^^^^ AIR001
|
||||
18 |
|
||||
19 | # Consider only from the `airflow.operators` (or providers operators) module
|
||||
|
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_args.py:6:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use schedule instead
|
||||
AIR302_args.py:6:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
4 | DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
5 |
|
||||
@@ -11,7 +12,7 @@ AIR302_args.py:6:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use s
|
||||
8 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
||||
|
|
||||
|
||||
AIR302_args.py:8:31: AIR302 `timetable` is removed in Airflow 3.0; use schedule instead
|
||||
AIR302_args.py:8:31: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
6 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
|
||||
7 |
|
||||
@@ -19,7 +20,7 @@ AIR302_args.py:8:31: AIR302 `timetable` is removed in Airflow 3.0; use schedule
|
||||
| ^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_args.py:16:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use schedule instead
|
||||
AIR302_args.py:16:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
16 | @dag(schedule_interval="0 * * * *")
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
@@ -27,7 +28,7 @@ AIR302_args.py:16:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use s
|
||||
18 | pass
|
||||
|
|
||||
|
||||
AIR302_args.py:21:6: AIR302 `timetable` is removed in Airflow 3.0; use schedule instead
|
||||
AIR302_args.py:21:6: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead
|
||||
|
|
||||
21 | @dag(timetable=NullTimetable())
|
||||
| ^^^^^^^^^ AIR302
|
||||
|
||||
@@ -1,38 +1,157 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_names.py:4:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
||||
|
|
||||
2 | from airflow.utils.dates import date_range, datetime_to_nano, days_ago
|
||||
3 |
|
||||
4 | date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
5 | days_ago
|
||||
|
|
||||
|
||||
AIR302_names.py:5:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use datetime.timedelta() instead
|
||||
|
|
||||
4 | date_range
|
||||
5 | days_ago
|
||||
| ^^^^^^^^ AIR302
|
||||
6 |
|
||||
7 | dates.date_range
|
||||
|
|
||||
|
||||
AIR302_names.py:7:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0
|
||||
|
|
||||
5 | days_ago
|
||||
6 |
|
||||
7 | dates.date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
8 | dates.days_ago
|
||||
|
|
||||
|
||||
AIR302_names.py:8:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use datetime.timedelta() instead
|
||||
AIR302_names.py:21:1: AIR302 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
|
||||
|
|
||||
7 | dates.date_range
|
||||
8 | dates.days_ago
|
||||
21 | TaskStateTrigger
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:24:1: AIR302 `airflow.www.auth.has_access` is removed in Airflow 3.0; use `airflow.www.auth.has_access_*` instead
|
||||
|
|
||||
24 | has_access
|
||||
| ^^^^^^^^^^ AIR302
|
||||
25 | requires_access
|
||||
|
|
||||
|
||||
AIR302_names.py:25:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0; use `airflow.api_connexion.security.requires_access_*` instead
|
||||
|
|
||||
24 | has_access
|
||||
25 | requires_access
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
26 |
|
||||
27 | AllowListValidator
|
||||
|
|
||||
|
||||
AIR302_names.py:27:1: AIR302 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternAllowListValidator` instead
|
||||
|
|
||||
25 | requires_access
|
||||
26 |
|
||||
27 | AllowListValidator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
28 | BlockListValidator
|
||||
|
|
||||
|
||||
AIR302_names.py:28:1: AIR302 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternBlockListValidator` instead
|
||||
|
|
||||
27 | AllowListValidator
|
||||
28 | BlockListValidator
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
29 |
|
||||
30 | dates.date_range
|
||||
|
|
||||
|
||||
AIR302_names.py:30:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
|
|
||||
28 | BlockListValidator
|
||||
29 |
|
||||
30 | dates.date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
31 | dates.days_ago
|
||||
|
|
||||
|
||||
AIR302_names.py:31:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
30 | dates.date_range
|
||||
31 | dates.days_ago
|
||||
| ^^^^^^^^ AIR302
|
||||
9 |
|
||||
10 | # This one was not deprecated.
|
||||
32 |
|
||||
33 | date_range
|
||||
|
|
||||
|
||||
AIR302_names.py:33:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead
|
||||
|
|
||||
31 | dates.days_ago
|
||||
32 |
|
||||
33 | date_range
|
||||
| ^^^^^^^^^^ AIR302
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
|
|
||||
|
||||
AIR302_names.py:34:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
33 | date_range
|
||||
34 | days_ago
|
||||
| ^^^^^^^^ AIR302
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
|
|
||||
|
||||
AIR302_names.py:35:1: AIR302 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
33 | date_range
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
| ^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
|
|
||||
|
||||
AIR302_names.py:36:1: AIR302 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
||||
|
|
||||
34 | days_ago
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
| ^^^^^^^^^^ AIR302
|
||||
37 | scale_time_units
|
||||
38 | infer_time_unit
|
||||
|
|
||||
|
||||
AIR302_names.py:37:1: AIR302 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
||||
|
|
||||
35 | parse_execution_date
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
38 | infer_time_unit
|
||||
|
|
||||
|
||||
AIR302_names.py:38:1: AIR302 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
|
||||
|
|
||||
36 | round_time
|
||||
37 | scale_time_units
|
||||
38 | infer_time_unit
|
||||
| ^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:45:1: AIR302 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0
|
||||
|
|
||||
43 | dates.datetime_to_nano
|
||||
44 |
|
||||
45 | TemporaryDirectory
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
46 | mkdirs
|
||||
|
|
||||
|
||||
AIR302_names.py:46:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead
|
||||
|
|
||||
45 | TemporaryDirectory
|
||||
46 | mkdirs
|
||||
| ^^^^^^ AIR302
|
||||
47 |
|
||||
48 | SHUTDOWN
|
||||
|
|
||||
|
||||
AIR302_names.py:48:1: AIR302 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
||||
|
|
||||
46 | mkdirs
|
||||
47 |
|
||||
48 | SHUTDOWN
|
||||
| ^^^^^^^^ AIR302
|
||||
49 | terminating_states
|
||||
|
|
||||
|
||||
AIR302_names.py:49:1: AIR302 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
||||
|
|
||||
48 | SHUTDOWN
|
||||
49 | terminating_states
|
||||
| ^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_names.py:52:1: AIR302 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
||||
|
|
||||
52 | test_cycle
|
||||
| ^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
@@ -35,8 +35,8 @@ use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
///
|
||||
/// ## References
|
||||
/// - [`asyncio` events](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event)
|
||||
/// - [`anyio` events](https://trio.readthedocs.io/en/latest/reference-core.html#trio.Event)
|
||||
/// - [`trio` events](https://anyio.readthedocs.io/en/latest/api.html#anyio.Event)
|
||||
/// - [`anyio` events](https://anyio.readthedocs.io/en/latest/api.html#anyio.Event)
|
||||
/// - [`trio` events](https://trio.readthedocs.io/en/latest/reference-core.html#trio.Event)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct AsyncBusyWait {
|
||||
module: AsyncModule,
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
}
|
||||
}
|
||||
};
|
||||
format!("Wrong type passed to first argument of `@pytest.mark.parametrize`; expected {expected_string}")
|
||||
format!("Wrong type passed to first argument of `pytest.mark.parametrize`; expected {expected_string}")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
@@ -210,7 +210,7 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let PytestParametrizeValuesWrongType { values, row } = self;
|
||||
format!("Wrong values type in `@pytest.mark.parametrize` expected `{values}` of `{row}`")
|
||||
format!("Wrong values type in `pytest.mark.parametrize` expected `{values}` of `{row}`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
@@ -273,7 +273,7 @@ impl Violation for PytestDuplicateParametrizeTestCases {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let PytestDuplicateParametrizeTestCases { index } = self;
|
||||
format!("Duplicate of test case at index {index} in `@pytest_mark.parametrize`")
|
||||
format!("Duplicate of test case at index {index} in `pytest.mark.parametrize`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
|
|
||||
24 | @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -21,7 +21,7 @@ PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
26 26 | ...
|
||||
27 27 |
|
||||
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
29 | @pytest.mark.parametrize(("param1",), [1, 2, 3])
|
||||
| ^^^^^^^^^^^ PT006
|
||||
@@ -40,7 +40,7 @@ PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
31 31 | ...
|
||||
32 32 |
|
||||
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
|
|
||||
34 | @pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -59,7 +59,7 @@ PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
36 36 | ...
|
||||
37 37 |
|
||||
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
39 | @pytest.mark.parametrize(["param1"], [1, 2, 3])
|
||||
| ^^^^^^^^^^ PT006
|
||||
@@ -78,7 +78,7 @@ PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
41 41 | ...
|
||||
42 42 |
|
||||
|
||||
PT006.py:44:26: PT006 Wrong type passed to first argument of `@pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
PT006.py:44:26: PT006 Wrong type passed to first argument of `pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
|
|
||||
44 | @pytest.mark.parametrize([some_expr, another_expr], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -87,7 +87,7 @@ PT006.py:44:26: PT006 Wrong type passed to first argument of `@pytest.mark.param
|
||||
|
|
||||
= help: Use a string of comma-separated values for the first argument
|
||||
|
||||
PT006.py:49:26: PT006 Wrong type passed to first argument of `@pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
PT006.py:49:26: PT006 Wrong type passed to first argument of `pytest.mark.parametrize`; expected a string of comma-separated values
|
||||
|
|
||||
49 | @pytest.mark.parametrize([some_expr, "param2"], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
9 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -21,7 +21,7 @@ PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.pa
|
||||
11 11 | ...
|
||||
12 12 |
|
||||
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
14 | @pytest.mark.parametrize(" param1, , param2 , ", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -40,7 +40,7 @@ PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
19 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -59,7 +59,7 @@ PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
21 21 | ...
|
||||
22 22 |
|
||||
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
29 | @pytest.mark.parametrize(("param1",), [1, 2, 3])
|
||||
| ^^^^^^^^^^^ PT006
|
||||
@@ -78,7 +78,7 @@ PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
31 31 | ...
|
||||
32 32 |
|
||||
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
34 | @pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -97,7 +97,7 @@ PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
36 36 | ...
|
||||
37 37 |
|
||||
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
39 | @pytest.mark.parametrize(["param1"], [1, 2, 3])
|
||||
| ^^^^^^^^^^ PT006
|
||||
@@ -116,7 +116,7 @@ PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
41 41 | ...
|
||||
42 42 |
|
||||
|
||||
PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
44 | @pytest.mark.parametrize([some_expr, another_expr], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -135,7 +135,7 @@ PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
46 46 | ...
|
||||
47 47 |
|
||||
|
||||
PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
49 | @pytest.mark.parametrize([some_expr, "param2"], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -154,7 +154,7 @@ PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
51 51 | ...
|
||||
52 52 |
|
||||
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
54 | @pytest.mark.parametrize(("param1, " "param2, " "param3"), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -173,7 +173,7 @@ PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
56 56 | ...
|
||||
57 57 |
|
||||
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
59 | @pytest.mark.parametrize("param1, " "param2, " "param3", [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -192,7 +192,7 @@ PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
61 61 | ...
|
||||
62 62 |
|
||||
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
64 | @pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -211,7 +211,7 @@ PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
66 66 | ...
|
||||
67 67 |
|
||||
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
69 | @pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^ PT006
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
9 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -21,7 +21,7 @@ PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.pa
|
||||
11 11 | ...
|
||||
12 12 |
|
||||
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
14 | @pytest.mark.parametrize(" param1, , param2 , ", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -40,7 +40,7 @@ PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
19 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -59,7 +59,7 @@ PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
21 21 | ...
|
||||
22 22 |
|
||||
|
||||
PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
24 | @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -78,7 +78,7 @@ PT006.py:24:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
26 26 | ...
|
||||
27 27 |
|
||||
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
29 | @pytest.mark.parametrize(("param1",), [1, 2, 3])
|
||||
| ^^^^^^^^^^^ PT006
|
||||
@@ -97,7 +97,7 @@ PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
31 31 | ...
|
||||
32 32 |
|
||||
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
39 | @pytest.mark.parametrize(["param1"], [1, 2, 3])
|
||||
| ^^^^^^^^^^ PT006
|
||||
@@ -116,7 +116,7 @@ PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
41 41 | ...
|
||||
42 42 |
|
||||
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
54 | @pytest.mark.parametrize(("param1, " "param2, " "param3"), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -135,7 +135,7 @@ PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
56 56 | ...
|
||||
57 57 |
|
||||
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
59 | @pytest.mark.parametrize("param1, " "param2, " "param3", [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -154,7 +154,7 @@ PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
61 61 | ...
|
||||
62 62 |
|
||||
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
64 | @pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -173,7 +173,7 @@ PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
66 66 | ...
|
||||
67 67 |
|
||||
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `list`
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `list`
|
||||
|
|
||||
69 | @pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^ PT006
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT007.py:4:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:4:35: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
4 | @pytest.mark.parametrize("param", (1, 2))
|
||||
| ^^^^^^ PT007
|
||||
@@ -21,7 +21,7 @@ PT007.py:4:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
6 6 | ...
|
||||
7 7 |
|
||||
|
||||
PT007.py:11:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:11:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
9 | @pytest.mark.parametrize(
|
||||
10 | ("param1", "param2"),
|
||||
@@ -50,7 +50,7 @@ PT007.py:11:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
16 16 | def test_tuple_of_tuples(param1, param2):
|
||||
17 17 | ...
|
||||
|
||||
PT007.py:12:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:12:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
10 | ("param1", "param2"),
|
||||
11 | (
|
||||
@@ -71,7 +71,7 @@ PT007.py:12:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
14 14 | ),
|
||||
15 15 | )
|
||||
|
||||
PT007.py:13:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:13:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
11 | (
|
||||
12 | (1, 2),
|
||||
@@ -92,7 +92,7 @@ PT007.py:13:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
15 15 | )
|
||||
16 16 | def test_tuple_of_tuples(param1, param2):
|
||||
|
||||
PT007.py:22:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:22:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
20 | @pytest.mark.parametrize(
|
||||
21 | ("param1", "param2"),
|
||||
@@ -121,7 +121,7 @@ PT007.py:22:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
27 27 | def test_tuple_of_lists(param1, param2):
|
||||
28 28 | ...
|
||||
|
||||
PT007.py:39:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:39:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
37 | ("param1", "param2"),
|
||||
38 | [
|
||||
@@ -142,7 +142,7 @@ PT007.py:39:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
41 41 | ],
|
||||
42 42 | )
|
||||
|
||||
PT007.py:40:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:40:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
38 | [
|
||||
39 | (1, 2),
|
||||
@@ -163,7 +163,7 @@ PT007.py:40:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
42 42 | )
|
||||
43 43 | def test_list_of_tuples(param1, param2):
|
||||
|
||||
PT007.py:81:38: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:81:38: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -183,7 +183,7 @@ PT007.py:81:38: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
84 84 | "d",
|
||||
|
||||
PT007.py:81:39: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:81:39: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -203,7 +203,7 @@ PT007.py:81:39: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
84 84 | "d",
|
||||
|
||||
PT007.py:81:47: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `list`
|
||||
PT007.py:81:47: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT007.py:4:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:4:35: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
4 | @pytest.mark.parametrize("param", (1, 2))
|
||||
| ^^^^^^ PT007
|
||||
@@ -21,7 +21,7 @@ PT007.py:4:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
6 6 | ...
|
||||
7 7 |
|
||||
|
||||
PT007.py:11:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:11:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
9 | @pytest.mark.parametrize(
|
||||
10 | ("param1", "param2"),
|
||||
@@ -50,7 +50,7 @@ PT007.py:11:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
16 16 | def test_tuple_of_tuples(param1, param2):
|
||||
17 17 | ...
|
||||
|
||||
PT007.py:22:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:22:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
20 | @pytest.mark.parametrize(
|
||||
21 | ("param1", "param2"),
|
||||
@@ -79,7 +79,7 @@ PT007.py:22:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
27 27 | def test_tuple_of_lists(param1, param2):
|
||||
28 28 | ...
|
||||
|
||||
PT007.py:23:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:23:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
21 | ("param1", "param2"),
|
||||
22 | (
|
||||
@@ -100,7 +100,7 @@ PT007.py:23:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
25 25 | ),
|
||||
26 26 | )
|
||||
|
||||
PT007.py:24:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:24:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
22 | (
|
||||
23 | [1, 2],
|
||||
@@ -121,7 +121,7 @@ PT007.py:24:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
26 26 | )
|
||||
27 27 | def test_tuple_of_lists(param1, param2):
|
||||
|
||||
PT007.py:50:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:50:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
48 | ("param1", "param2"),
|
||||
49 | [
|
||||
@@ -142,7 +142,7 @@ PT007.py:50:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
52 52 | ],
|
||||
53 53 | )
|
||||
|
||||
PT007.py:51:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:51:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
49 | [
|
||||
50 | [1, 2],
|
||||
@@ -163,7 +163,7 @@ PT007.py:51:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
53 53 | )
|
||||
54 54 | def test_list_of_lists(param1, param2):
|
||||
|
||||
PT007.py:61:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:61:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
59 | "param1,param2",
|
||||
60 | [
|
||||
@@ -184,7 +184,7 @@ PT007.py:61:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
63 63 | ],
|
||||
64 64 | )
|
||||
|
||||
PT007.py:62:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:62:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
60 | [
|
||||
61 | [1, 2],
|
||||
@@ -205,7 +205,7 @@ PT007.py:62:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
64 64 | )
|
||||
65 65 | def test_csv_name_list_of_lists(param1, param2):
|
||||
|
||||
PT007.py:81:38: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple`
|
||||
PT007.py:81:38: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `list` of `tuple`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT007.py:12:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:12:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
10 | ("param1", "param2"),
|
||||
11 | (
|
||||
@@ -23,7 +23,7 @@ PT007.py:12:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
14 14 | ),
|
||||
15 15 | )
|
||||
|
||||
PT007.py:13:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:13:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
11 | (
|
||||
12 | (1, 2),
|
||||
@@ -44,7 +44,7 @@ PT007.py:13:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
15 15 | )
|
||||
16 16 | def test_tuple_of_tuples(param1, param2):
|
||||
|
||||
PT007.py:31:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:31:35: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
31 | @pytest.mark.parametrize("param", [1, 2])
|
||||
| ^^^^^^ PT007
|
||||
@@ -63,7 +63,7 @@ PT007.py:31:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
33 33 | ...
|
||||
34 34 |
|
||||
|
||||
PT007.py:38:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:38:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
36 | @pytest.mark.parametrize(
|
||||
37 | ("param1", "param2"),
|
||||
@@ -92,7 +92,7 @@ PT007.py:38:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
43 43 | def test_list_of_tuples(param1, param2):
|
||||
44 44 | ...
|
||||
|
||||
PT007.py:39:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:39:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
37 | ("param1", "param2"),
|
||||
38 | [
|
||||
@@ -113,7 +113,7 @@ PT007.py:39:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
41 41 | ],
|
||||
42 42 | )
|
||||
|
||||
PT007.py:40:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:40:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
38 | [
|
||||
39 | (1, 2),
|
||||
@@ -134,7 +134,7 @@ PT007.py:40:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
42 42 | )
|
||||
43 43 | def test_list_of_tuples(param1, param2):
|
||||
|
||||
PT007.py:49:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:49:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
47 | @pytest.mark.parametrize(
|
||||
48 | ("param1", "param2"),
|
||||
@@ -163,7 +163,7 @@ PT007.py:49:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
54 54 | def test_list_of_lists(param1, param2):
|
||||
55 55 | ...
|
||||
|
||||
PT007.py:60:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:60:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
58 | @pytest.mark.parametrize(
|
||||
59 | "param1,param2",
|
||||
@@ -192,7 +192,7 @@ PT007.py:60:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
65 65 | def test_csv_name_list_of_lists(param1, param2):
|
||||
66 66 | ...
|
||||
|
||||
PT007.py:71:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:71:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
69 | @pytest.mark.parametrize(
|
||||
70 | "param",
|
||||
@@ -221,7 +221,7 @@ PT007.py:71:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
76 76 | def test_single_list_of_lists(param):
|
||||
77 77 | ...
|
||||
|
||||
PT007.py:80:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:80:31: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
| ^^^^^^ PT007
|
||||
@@ -240,7 +240,7 @@ PT007.py:80:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
82 82 | @pytest.mark.parametrize("d", [3,])
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
|
||||
PT007.py:81:39: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:81:39: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -260,7 +260,7 @@ PT007.py:81:39: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
84 84 | "d",
|
||||
|
||||
PT007.py:81:47: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:81:47: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -280,7 +280,7 @@ PT007.py:81:47: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
84 84 | "d",
|
||||
|
||||
PT007.py:82:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:82:31: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -301,7 +301,7 @@ PT007.py:82:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
84 84 | "d",
|
||||
85 85 | [("3", "4")],
|
||||
|
||||
PT007.py:85:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:85:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
83 | @pytest.mark.parametrize(
|
||||
84 | "d",
|
||||
@@ -322,7 +322,7 @@ PT007.py:85:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
87 87 | @pytest.mark.parametrize(
|
||||
88 88 | "e",
|
||||
|
||||
PT007.py:89:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `list`
|
||||
PT007.py:89:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `list`
|
||||
|
|
||||
87 | @pytest.mark.parametrize(
|
||||
88 | "e",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT007.py:23:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:23:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
21 | ("param1", "param2"),
|
||||
22 | (
|
||||
@@ -23,7 +23,7 @@ PT007.py:23:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
25 25 | ),
|
||||
26 26 | )
|
||||
|
||||
PT007.py:24:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:24:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
22 | (
|
||||
23 | [1, 2],
|
||||
@@ -44,7 +44,7 @@ PT007.py:24:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
26 26 | )
|
||||
27 27 | def test_tuple_of_lists(param1, param2):
|
||||
|
||||
PT007.py:31:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:31:35: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
31 | @pytest.mark.parametrize("param", [1, 2])
|
||||
| ^^^^^^ PT007
|
||||
@@ -63,7 +63,7 @@ PT007.py:31:35: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
33 33 | ...
|
||||
34 34 |
|
||||
|
||||
PT007.py:38:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:38:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
36 | @pytest.mark.parametrize(
|
||||
37 | ("param1", "param2"),
|
||||
@@ -92,7 +92,7 @@ PT007.py:38:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
43 43 | def test_list_of_tuples(param1, param2):
|
||||
44 44 | ...
|
||||
|
||||
PT007.py:49:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:49:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
47 | @pytest.mark.parametrize(
|
||||
48 | ("param1", "param2"),
|
||||
@@ -121,7 +121,7 @@ PT007.py:49:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
54 54 | def test_list_of_lists(param1, param2):
|
||||
55 55 | ...
|
||||
|
||||
PT007.py:50:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:50:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
48 | ("param1", "param2"),
|
||||
49 | [
|
||||
@@ -142,7 +142,7 @@ PT007.py:50:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
52 52 | ],
|
||||
53 53 | )
|
||||
|
||||
PT007.py:51:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:51:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
49 | [
|
||||
50 | [1, 2],
|
||||
@@ -163,7 +163,7 @@ PT007.py:51:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
53 53 | )
|
||||
54 54 | def test_list_of_lists(param1, param2):
|
||||
|
||||
PT007.py:60:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:60:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
58 | @pytest.mark.parametrize(
|
||||
59 | "param1,param2",
|
||||
@@ -192,7 +192,7 @@ PT007.py:60:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
65 65 | def test_csv_name_list_of_lists(param1, param2):
|
||||
66 66 | ...
|
||||
|
||||
PT007.py:61:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:61:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
59 | "param1,param2",
|
||||
60 | [
|
||||
@@ -213,7 +213,7 @@ PT007.py:61:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
63 63 | ],
|
||||
64 64 | )
|
||||
|
||||
PT007.py:62:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:62:9: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
60 | [
|
||||
61 | [1, 2],
|
||||
@@ -234,7 +234,7 @@ PT007.py:62:9: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
64 64 | )
|
||||
65 65 | def test_csv_name_list_of_lists(param1, param2):
|
||||
|
||||
PT007.py:71:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:71:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
69 | @pytest.mark.parametrize(
|
||||
70 | "param",
|
||||
@@ -263,7 +263,7 @@ PT007.py:71:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
76 76 | def test_single_list_of_lists(param):
|
||||
77 77 | ...
|
||||
|
||||
PT007.py:80:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:80:31: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
| ^^^^^^ PT007
|
||||
@@ -282,7 +282,7 @@ PT007.py:80:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
82 82 | @pytest.mark.parametrize("d", [3,])
|
||||
83 83 | @pytest.mark.parametrize(
|
||||
|
||||
PT007.py:82:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:82:31: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
80 | @pytest.mark.parametrize("a", [1, 2])
|
||||
81 | @pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
|
||||
@@ -303,7 +303,7 @@ PT007.py:82:31: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expect
|
||||
84 84 | "d",
|
||||
85 85 | [("3", "4")],
|
||||
|
||||
PT007.py:85:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:85:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
83 | @pytest.mark.parametrize(
|
||||
84 | "d",
|
||||
@@ -324,7 +324,7 @@ PT007.py:85:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expecte
|
||||
87 87 | @pytest.mark.parametrize(
|
||||
88 88 | "e",
|
||||
|
||||
PT007.py:89:5: PT007 [*] Wrong values type in `@pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
PT007.py:89:5: PT007 [*] Wrong values type in `pytest.mark.parametrize` expected `tuple` of `tuple`
|
||||
|
|
||||
87 | @pytest.mark.parametrize(
|
||||
88 | "e",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT014.py:4:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:4:35: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
4 | @pytest.mark.parametrize("x", [1, 1, 2])
|
||||
| ^ PT014
|
||||
@@ -21,7 +21,7 @@ PT014.py:4:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.para
|
||||
6 6 | ...
|
||||
7 7 |
|
||||
|
||||
PT014.py:14:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:14:35: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
@@ -40,7 +40,7 @@ PT014.py:14:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.par
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT014.py:14:41: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
||||
PT014.py:14:41: PT014 [*] Duplicate of test case at index 2 in `pytest.mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
@@ -59,7 +59,7 @@ PT014.py:14:41: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.par
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT014.py:14:44: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
||||
PT014.py:14:44: PT014 [*] Duplicate of test case at index 2 in `pytest.mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
@@ -78,7 +78,7 @@ PT014.py:14:44: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.par
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT014.py:24:9: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:24:9: PT014 Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
22 | (a, b),
|
||||
23 | # comment
|
||||
@@ -89,7 +89,7 @@ PT014.py:24:9: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametr
|
||||
|
|
||||
= help: Remove duplicate test case
|
||||
|
||||
PT014.py:32:39: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:32:39: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
32 | @pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
|
||||
| ^ PT014
|
||||
@@ -108,7 +108,7 @@ PT014.py:32:39: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.par
|
||||
34 34 | ...
|
||||
35 35 |
|
||||
|
||||
PT014.py:32:48: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:32:48: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
32 | @pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
|
||||
| ^ PT014
|
||||
@@ -127,7 +127,7 @@ PT014.py:32:48: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.par
|
||||
34 34 | ...
|
||||
35 35 |
|
||||
|
||||
PT014.py:42:10: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:42:10: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
40 | a,
|
||||
41 | b,
|
||||
@@ -147,7 +147,7 @@ PT014.py:42:10: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.par
|
||||
44 43 | ((a)),
|
||||
45 44 | ],
|
||||
|
||||
PT014.py:44:11: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:44:11: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
42 | (a),
|
||||
43 | c,
|
||||
@@ -167,7 +167,7 @@ PT014.py:44:11: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.par
|
||||
46 45 | )
|
||||
47 46 | def test_error_parentheses_trailing_comma(x):
|
||||
|
||||
PT014.py:56:53: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
PT014.py:56:53: PT014 [*] Duplicate of test case at index 0 in `pytest.mark.parametrize`
|
||||
|
|
||||
56 | @pytest.mark.parametrize('data, spec', [(1.0, 1.0), (1.0, 1.0)])
|
||||
| ^^^^^^^^^^ PT014
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
9 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -21,7 +21,7 @@ PT006.py:9:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.pa
|
||||
11 11 | ...
|
||||
12 12 |
|
||||
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
14 | @pytest.mark.parametrize(" param1, , param2 , ", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -40,7 +40,7 @@ PT006.py:14:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
16 16 | ...
|
||||
17 17 |
|
||||
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
19 | @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^ PT006
|
||||
@@ -59,7 +59,7 @@ PT006.py:19:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
21 21 | ...
|
||||
22 22 |
|
||||
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
29 | @pytest.mark.parametrize(("param1",), [1, 2, 3])
|
||||
| ^^^^^^^^^^^ PT006
|
||||
@@ -78,7 +78,7 @@ PT006.py:29:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
31 31 | ...
|
||||
32 32 |
|
||||
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
34 | @pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -97,7 +97,7 @@ PT006.py:34:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
36 36 | ...
|
||||
37 37 |
|
||||
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `str`
|
||||
PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
|
||||
|
|
||||
39 | @pytest.mark.parametrize(["param1"], [1, 2, 3])
|
||||
| ^^^^^^^^^^ PT006
|
||||
@@ -116,7 +116,7 @@ PT006.py:39:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
41 41 | ...
|
||||
42 42 |
|
||||
|
||||
PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
44 | @pytest.mark.parametrize([some_expr, another_expr], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -135,7 +135,7 @@ PT006.py:44:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
46 46 | ...
|
||||
47 47 |
|
||||
|
||||
PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
49 | @pytest.mark.parametrize([some_expr, "param2"], [1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -154,7 +154,7 @@ PT006.py:49:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
51 51 | ...
|
||||
52 52 |
|
||||
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
54 | @pytest.mark.parametrize(("param1, " "param2, " "param3"), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -173,7 +173,7 @@ PT006.py:54:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
56 56 | ...
|
||||
57 57 |
|
||||
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
59 | @pytest.mark.parametrize("param1, " "param2, " "param3", [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -192,7 +192,7 @@ PT006.py:59:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
61 61 | ...
|
||||
62 62 |
|
||||
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
64 | @pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -211,7 +211,7 @@ PT006.py:64:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
66 66 | ...
|
||||
67 67 |
|
||||
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
69 | @pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -230,7 +230,7 @@ PT006.py:69:26: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
71 71 | ...
|
||||
72 72 |
|
||||
|
||||
PT006.py:74:39: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:74:39: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
74 | parametrize = pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^ PT006
|
||||
@@ -249,7 +249,7 @@ PT006.py:74:39: PT006 [*] Wrong type passed to first argument of `@pytest.mark.p
|
||||
76 76 | @parametrize
|
||||
77 77 | def test_not_decorator(param1, param2):
|
||||
|
||||
PT006.py:81:35: PT006 [*] Wrong type passed to first argument of `@pytest.mark.parametrize`; expected `tuple`
|
||||
PT006.py:81:35: PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple`
|
||||
|
|
||||
81 | @pytest.mark.parametrize(argnames=("param1,param2"), argvalues=[(1, 2), (3, 4)])
|
||||
| ^^^^^^^^^^^^^^^^^ PT006
|
||||
|
||||
@@ -8,11 +8,22 @@ use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for an unquoted type expression in `typing.cast()` calls.
|
||||
/// Checks for unquoted type expressions in `typing.cast()` calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `typing.cast()` does not do anything at runtime, so the time spent
|
||||
/// on evaluating the type expression is wasted.
|
||||
/// This rule helps enforce a consistent style across your codebase.
|
||||
///
|
||||
/// It's often necessary to quote the first argument passed to `cast()`,
|
||||
/// as type expressions can involve forward references, or references
|
||||
/// to symbols which are only imported in `typing.TYPE_CHECKING` blocks.
|
||||
/// This can lead to a visual inconsistency across different `cast()` calls,
|
||||
/// where some type expressions are quoted but others are not. By enabling
|
||||
/// this rule, you ensure that all type expressions passed to `cast()` are
|
||||
/// quoted, enforcing stylistic consistency across all of your `cast()` calls.
|
||||
///
|
||||
/// In some cases where `cast()` is used in a hot loop, this rule may also
|
||||
/// help avoid overhead from repeatedly evaluating complex type expressions at
|
||||
/// runtime.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
@@ -66,70 +66,75 @@ pub(crate) fn invalid_escape_sequence(checker: &mut Checker, string_like: String
|
||||
if part.flags().is_raw_string() {
|
||||
continue;
|
||||
}
|
||||
match part {
|
||||
StringLikePart::String(string_literal) => {
|
||||
check(
|
||||
&mut checker.diagnostics,
|
||||
locator,
|
||||
string_literal.start(),
|
||||
string_literal.range(),
|
||||
AnyStringFlags::from(string_literal.flags),
|
||||
);
|
||||
}
|
||||
StringLikePart::Bytes(bytes_literal) => {
|
||||
check(
|
||||
&mut checker.diagnostics,
|
||||
locator,
|
||||
bytes_literal.start(),
|
||||
bytes_literal.range(),
|
||||
AnyStringFlags::from(bytes_literal.flags),
|
||||
);
|
||||
let state = match part {
|
||||
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||
analyze_escape_chars(locator, part.range(), part.flags())
|
||||
}
|
||||
StringLikePart::FString(f_string) => {
|
||||
let flags = AnyStringFlags::from(f_string.flags);
|
||||
let mut escape_chars_state = EscapeCharsState::default();
|
||||
// Whether we suggest converting to a raw string or
|
||||
// adding backslashes depends on the presence of valid
|
||||
// escape characters in the entire f-string. Therefore,
|
||||
// we must analyze escape characters in each f-string
|
||||
// element before pushing a diagnostic and fix.
|
||||
for element in &f_string.elements {
|
||||
match element {
|
||||
FStringElement::Literal(literal) => {
|
||||
check(
|
||||
&mut checker.diagnostics,
|
||||
escape_chars_state.update(analyze_escape_chars(
|
||||
locator,
|
||||
f_string.start(),
|
||||
literal.range(),
|
||||
flags,
|
||||
);
|
||||
));
|
||||
}
|
||||
FStringElement::Expression(expression) => {
|
||||
let Some(format_spec) = expression.format_spec.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
for literal in format_spec.elements.literals() {
|
||||
check(
|
||||
&mut checker.diagnostics,
|
||||
escape_chars_state.update(analyze_escape_chars(
|
||||
locator,
|
||||
f_string.start(),
|
||||
literal.range(),
|
||||
flags,
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
escape_chars_state
|
||||
}
|
||||
}
|
||||
};
|
||||
check(
|
||||
&mut checker.diagnostics,
|
||||
locator,
|
||||
part.start(),
|
||||
part.flags(),
|
||||
state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
#[derive(Default)]
|
||||
struct EscapeCharsState {
|
||||
contains_valid_escape_sequence: bool,
|
||||
invalid_escape_chars: Vec<InvalidEscapeChar>,
|
||||
}
|
||||
|
||||
impl EscapeCharsState {
|
||||
fn update(&mut self, other: Self) {
|
||||
self.contains_valid_escape_sequence |= other.contains_valid_escape_sequence;
|
||||
self.invalid_escape_chars.extend(other.invalid_escape_chars);
|
||||
}
|
||||
}
|
||||
|
||||
/// Traverses string, collects invalid escape characters, and flags if a valid
|
||||
/// escape character is found.
|
||||
fn analyze_escape_chars(
|
||||
locator: &Locator,
|
||||
// Start position of the expression that contains the source range. This is used to generate
|
||||
// the fix when the source range is part of the expression like in f-string which contains
|
||||
// other f-string literal elements.
|
||||
expr_start: TextSize,
|
||||
// Range in the source code to perform the check on.
|
||||
// Range in the source code to perform the analysis on.
|
||||
source_range: TextRange,
|
||||
flags: AnyStringFlags,
|
||||
) {
|
||||
) -> EscapeCharsState {
|
||||
let source = locator.slice(source_range);
|
||||
let mut contains_valid_escape_sequence = false;
|
||||
let mut invalid_escape_chars = Vec::new();
|
||||
@@ -225,7 +230,31 @@ fn check(
|
||||
range,
|
||||
});
|
||||
}
|
||||
EscapeCharsState {
|
||||
contains_valid_escape_sequence,
|
||||
invalid_escape_chars,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a diagnostic and fix depending on escape characters seen so far.
|
||||
///
|
||||
/// If we have not seen any valid escape characters, we convert to
|
||||
/// a raw string. If we have seen valid escape characters,
|
||||
/// we manually add backslashes to each invalid escape character found.
|
||||
fn check(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
locator: &Locator,
|
||||
// Start position of the expression that contains the source range. This is used to generate
|
||||
// the fix when the source range is part of the expression like in f-string which contains
|
||||
// other f-string literal elements.
|
||||
expr_start: TextSize,
|
||||
flags: AnyStringFlags,
|
||||
escape_chars_state: EscapeCharsState,
|
||||
) {
|
||||
let EscapeCharsState {
|
||||
contains_valid_escape_sequence,
|
||||
invalid_escape_chars,
|
||||
} = escape_chars_state;
|
||||
if contains_valid_escape_sequence {
|
||||
// Escape with backslash.
|
||||
for invalid_escape_char in &invalid_escape_chars {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
@@ -243,6 +242,7 @@ W605_1.py:57:9: W605 [*] Invalid escape sequence: `\d`
|
||||
57 |+rf"{{}}+-\d"
|
||||
58 58 | f"\n{{}}+-\d+"
|
||||
59 59 | f"\n{{}}<7D>+-\d+"
|
||||
60 60 |
|
||||
|
||||
W605_1.py:58:11: W605 [*] Invalid escape sequence: `\d`
|
||||
|
|
||||
@@ -261,6 +261,8 @@ W605_1.py:58:11: W605 [*] Invalid escape sequence: `\d`
|
||||
58 |-f"\n{{}}+-\d+"
|
||||
58 |+f"\n{{}}+-\\d+"
|
||||
59 59 | f"\n{{}}<7D>+-\d+"
|
||||
60 60 |
|
||||
61 61 | # See https://github.com/astral-sh/ruff/issues/11491
|
||||
|
||||
W605_1.py:59:12: W605 [*] Invalid escape sequence: `\d`
|
||||
|
|
||||
@@ -268,6 +270,8 @@ W605_1.py:59:12: W605 [*] Invalid escape sequence: `\d`
|
||||
58 | f"\n{{}}+-\d+"
|
||||
59 | f"\n{{}}<7D>+-\d+"
|
||||
| ^^ W605
|
||||
60 |
|
||||
61 | # See https://github.com/astral-sh/ruff/issues/11491
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
@@ -277,3 +281,42 @@ W605_1.py:59:12: W605 [*] Invalid escape sequence: `\d`
|
||||
58 58 | f"\n{{}}+-\d+"
|
||||
59 |-f"\n{{}}<7D>+-\d+"
|
||||
59 |+f"\n{{}}<7D>+-\\d+"
|
||||
60 60 |
|
||||
61 61 | # See https://github.com/astral-sh/ruff/issues/11491
|
||||
62 62 | total = 10
|
||||
|
||||
W605_1.py:65:31: W605 [*] Invalid escape sequence: `\I`
|
||||
|
|
||||
63 | ok = 7
|
||||
64 | incomplete = 3
|
||||
65 | s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
|
||||
| ^^ W605
|
||||
66 |
|
||||
67 | # Debug text (should trigger)
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Safe fix
|
||||
62 62 | total = 10
|
||||
63 63 | ok = 7
|
||||
64 64 | incomplete = 3
|
||||
65 |-s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
|
||||
65 |+s = f"TOTAL: {total}\nOK: {ok}\\INCOMPLETE: {incomplete}\n"
|
||||
66 66 |
|
||||
67 67 | # Debug text (should trigger)
|
||||
68 68 | t = f"{'\InHere'=}"
|
||||
|
||||
W605_1.py:68:9: W605 [*] Invalid escape sequence: `\I`
|
||||
|
|
||||
67 | # Debug text (should trigger)
|
||||
68 | t = f"{'\InHere'=}"
|
||||
| ^^ W605
|
||||
|
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
|
||||
66 66 |
|
||||
67 67 | # Debug text (should trigger)
|
||||
68 |-t = f"{'\InHere'=}"
|
||||
68 |+t = f"{r'\InHere'=}"
|
||||
|
||||
@@ -411,7 +411,9 @@ mod tests {
|
||||
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
|
||||
#[test_case(Rule::UnrawRePattern, Path::new("RUF039.py"))]
|
||||
#[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
|
||||
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(crate) use sort_dunder_slots::*;
|
||||
pub(crate) use static_key_dict_comprehension::*;
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
pub(crate) use test_rules::*;
|
||||
pub(crate) use unnecessary_cast_to_int::*;
|
||||
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
||||
pub(crate) use unnecessary_key_check::*;
|
||||
pub(crate) use unnecessary_nested_literal::*;
|
||||
@@ -78,6 +79,7 @@ mod static_key_dict_comprehension;
|
||||
mod suppression_comment_visitor;
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
pub(crate) mod test_rules;
|
||||
mod unnecessary_cast_to_int;
|
||||
mod unnecessary_iterable_allocation_for_first_element;
|
||||
mod unnecessary_key_check;
|
||||
mod unnecessary_nested_literal;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprName, ExprNumberLiteral, Number};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `int` conversions of values that are already integers.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Such a conversion is unnecessary.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule may produce false positives for `round`, `math.ceil`, `math.floor`,
|
||||
/// and `math.trunc` calls when values override the `__round__`, `__ceil__`, `__floor__`,
|
||||
/// or `__trunc__` operators such that they don't return an integer.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// int(len([]))
|
||||
/// int(round(foo, None))
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// len([])
|
||||
/// round(foo)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix for `round`, `math.ceil`, `math.floor`, and `math.truncate` is unsafe
|
||||
/// because removing the `int` conversion can change the semantics for values
|
||||
/// overriding the `__round__`, `__ceil__`, `__floor__`, or `__trunc__` dunder methods
|
||||
/// such that they don't return an integer.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnnecessaryCastToInt;
|
||||
|
||||
impl AlwaysFixableViolation for UnnecessaryCastToInt {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Value being casted is already an integer".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove unnecessary conversion to `int`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF046
|
||||
pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(Expr::Call(inner_call)) = single_argument_to_int_call(semantic, call) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (func, arguments) = (&inner_call.func, &inner_call.arguments);
|
||||
let (outer_range, inner_range) = (call.range, inner_call.range);
|
||||
|
||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(func) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let fix = match qualified_name.segments() {
|
||||
// Always returns a strict instance of `int`
|
||||
["" | "builtins", "len" | "id" | "hash" | "ord" | "int"]
|
||||
| ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => {
|
||||
Fix::safe_edit(replace_with_inner(checker, outer_range, inner_range))
|
||||
}
|
||||
|
||||
// Depends on `ndigits` and `number.__round__`
|
||||
["" | "builtins", "round"] => {
|
||||
if let Some(fix) = replace_with_shortened_round_call(checker, outer_range, arguments) {
|
||||
fix
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Depends on `__ceil__`/`__floor__`/`__trunc__`
|
||||
["math", "ceil" | "floor" | "trunc"] => {
|
||||
Fix::unsafe_edit(replace_with_inner(checker, outer_range, inner_range))
|
||||
}
|
||||
|
||||
_ => return,
|
||||
};
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UnnecessaryCastToInt, call.range).with_fix(fix));
|
||||
}
|
||||
|
||||
fn single_argument_to_int_call<'a>(
|
||||
semantic: &SemanticModel,
|
||||
call: &'a ExprCall,
|
||||
) -> Option<&'a Expr> {
|
||||
let ExprCall {
|
||||
func, arguments, ..
|
||||
} = call;
|
||||
|
||||
if !semantic.match_builtin_expr(func, "int") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !arguments.keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let [argument] = &*arguments.args else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(argument)
|
||||
}
|
||||
|
||||
/// Returns an [`Edit`] when the call is of any of the forms:
|
||||
/// * `round(integer)`, `round(integer, 0)`, `round(integer, None)`
|
||||
/// * `round(whatever)`, `round(whatever, None)`
|
||||
fn replace_with_shortened_round_call(
|
||||
checker: &Checker,
|
||||
outer_range: TextRange,
|
||||
arguments: &Arguments,
|
||||
) -> Option<Fix> {
|
||||
if arguments.len() > 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let number = arguments.find_argument("number", 0)?;
|
||||
let ndigits = arguments.find_argument("ndigits", 1);
|
||||
|
||||
let number_is_int = match number {
|
||||
Expr::Name(name) => is_int(checker.semantic(), name),
|
||||
Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => matches!(value, Number::Int(..)),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
match ndigits {
|
||||
Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. }))
|
||||
if is_literal_zero(value) && number_is_int => {}
|
||||
Some(Expr::NoneLiteral(_)) | None => {}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let number_expr = checker.locator().slice(number);
|
||||
let new_content = format!("round({number_expr})");
|
||||
|
||||
let applicability = if number_is_int {
|
||||
Applicability::Safe
|
||||
} else {
|
||||
Applicability::Unsafe
|
||||
};
|
||||
|
||||
Some(Fix::applicable_edit(
|
||||
Edit::range_replacement(new_content, outer_range),
|
||||
applicability,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_int(semantic: &SemanticModel, name: &ExprName) -> bool {
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
fn is_literal_zero(value: &Number) -> bool {
|
||||
let Number::Int(int) = value else {
|
||||
return false;
|
||||
};
|
||||
|
||||
matches!(int.as_u8(), Some(0))
|
||||
}
|
||||
|
||||
fn replace_with_inner(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit {
|
||||
let inner_expr = checker.locator().slice(inner_range);
|
||||
|
||||
Edit::range_replacement(inner_expr.to_string(), outer_range)
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
|
||||
use itertools::Itertools;
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{
|
||||
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, Identifier,
|
||||
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral,
|
||||
Identifier,
|
||||
};
|
||||
use ruff_python_semantic::analyze::typing::find_binding_value;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
@@ -53,17 +56,19 @@ use crate::checkers::ast::Checker;
|
||||
/// - [Python Regular Expression HOWTO: Common Problems - Use String Methods](https://docs.python.org/3/howto/regex.html#use-string-methods)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnnecessaryRegularExpression {
|
||||
replacement: String,
|
||||
replacement: Option<String>,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for UnnecessaryRegularExpression {
|
||||
impl Violation for UnnecessaryRegularExpression {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Plain string pattern passed to `re` function".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Replace with `{}`", self.replacement)
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Replace with `{}`", self.replacement.as_ref()?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +95,8 @@ pub(crate) fn unnecessary_regular_expression(checker: &mut Checker, call: &ExprC
|
||||
return;
|
||||
};
|
||||
|
||||
// For now, restrict this rule to string literals
|
||||
let Some(string_lit) = re_func.pattern.as_string_literal_expr() else {
|
||||
// For now, restrict this rule to string literals and variables that can be resolved to literals
|
||||
let Some(string_lit) = resolve_string_literal(re_func.pattern, semantic) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -110,33 +115,36 @@ pub(crate) fn unnecessary_regular_expression(checker: &mut Checker, call: &ExprC
|
||||
// we can proceed with the str method replacement
|
||||
let new_expr = re_func.replacement();
|
||||
|
||||
let repl = checker.generator().expr(&new_expr);
|
||||
let diagnostic = Diagnostic::new(
|
||||
let repl = new_expr.map(|expr| checker.generator().expr(&expr));
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryRegularExpression {
|
||||
replacement: repl.clone(),
|
||||
},
|
||||
call.range,
|
||||
);
|
||||
|
||||
let fix = Fix::applicable_edit(
|
||||
Edit::range_replacement(repl, call.range),
|
||||
if checker
|
||||
.comment_ranges()
|
||||
.has_comments(call, checker.source())
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
);
|
||||
if let Some(repl) = repl {
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
Edit::range_replacement(repl, call.range),
|
||||
if checker
|
||||
.comment_ranges()
|
||||
.has_comments(call, checker.source())
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic.with_fix(fix));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// The `re` functions supported by this rule.
|
||||
#[derive(Debug)]
|
||||
enum ReFuncKind<'a> {
|
||||
Sub { repl: &'a Expr },
|
||||
// Only `Some` if it's a fixable `re.sub()` call
|
||||
Sub { repl: Option<&'a Expr> },
|
||||
Match,
|
||||
Search,
|
||||
Fullmatch,
|
||||
@@ -152,7 +160,7 @@ struct ReFunc<'a> {
|
||||
|
||||
impl<'a> ReFunc<'a> {
|
||||
fn from_call_expr(
|
||||
semantic: &SemanticModel,
|
||||
semantic: &'a SemanticModel,
|
||||
call: &'a ExprCall,
|
||||
func_name: &str,
|
||||
) -> Option<Self> {
|
||||
@@ -173,11 +181,32 @@ impl<'a> ReFunc<'a> {
|
||||
// version
|
||||
("sub", 3) => {
|
||||
let repl = call.arguments.find_argument("repl", 1)?;
|
||||
if !repl.is_string_literal_expr() {
|
||||
return None;
|
||||
let lit = resolve_string_literal(repl, semantic)?;
|
||||
let mut fixable = true;
|
||||
for (c, next) in lit.value.chars().tuple_windows() {
|
||||
// `\0` (or any other ASCII digit) and `\g` have special meaning in `repl` strings.
|
||||
// Meanwhile, nearly all other escapes of ASCII letters in a `repl` string causes
|
||||
// `re.PatternError` to be raised at runtime.
|
||||
//
|
||||
// If we see that the escaped character is an alphanumeric ASCII character,
|
||||
// we should only emit a diagnostic suggesting to replace the `re.sub()` call with
|
||||
// `str.replace`if we can detect that the escaped character is one that is both
|
||||
// valid in a `repl` string *and* does not have any special meaning in a REPL string.
|
||||
//
|
||||
// It's out of scope for this rule to change invalid `re.sub()` calls into something
|
||||
// that would not raise an exception at runtime. They should be left as-is.
|
||||
if c == '\\' && next.is_ascii_alphanumeric() {
|
||||
if "abfnrtv".contains(next) {
|
||||
fixable = false;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ReFunc {
|
||||
kind: ReFuncKind::Sub { repl },
|
||||
kind: ReFuncKind::Sub {
|
||||
repl: fixable.then_some(repl),
|
||||
},
|
||||
pattern: call.arguments.find_argument("pattern", 0)?,
|
||||
string: call.arguments.find_argument("string", 2)?,
|
||||
})
|
||||
@@ -201,20 +230,20 @@ impl<'a> ReFunc<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn replacement(&self) -> Expr {
|
||||
fn replacement(&self) -> Option<Expr> {
|
||||
match self.kind {
|
||||
// string.replace(pattern, repl)
|
||||
ReFuncKind::Sub { repl } => {
|
||||
self.method_expr("replace", vec![self.pattern.clone(), repl.clone()])
|
||||
}
|
||||
ReFuncKind::Sub { repl } => repl
|
||||
.cloned()
|
||||
.map(|repl| self.method_expr("replace", vec![self.pattern.clone(), repl])),
|
||||
// string.startswith(pattern)
|
||||
ReFuncKind::Match => self.method_expr("startswith", vec![self.pattern.clone()]),
|
||||
ReFuncKind::Match => Some(self.method_expr("startswith", vec![self.pattern.clone()])),
|
||||
// pattern in string
|
||||
ReFuncKind::Search => self.compare_expr(CmpOp::In),
|
||||
ReFuncKind::Search => Some(self.compare_expr(CmpOp::In)),
|
||||
// string == pattern
|
||||
ReFuncKind::Fullmatch => self.compare_expr(CmpOp::Eq),
|
||||
ReFuncKind::Fullmatch => Some(self.compare_expr(CmpOp::Eq)),
|
||||
// string.split(pattern)
|
||||
ReFuncKind::Split => self.method_expr("split", vec![self.pattern.clone()]),
|
||||
ReFuncKind::Split => Some(self.method_expr("split", vec![self.pattern.clone()])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,3 +277,23 @@ impl<'a> ReFunc<'a> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to resolve `name` to an [`ExprStringLiteral`] in `semantic`.
|
||||
fn resolve_string_literal<'a>(
|
||||
name: &'a Expr,
|
||||
semantic: &'a SemanticModel,
|
||||
) -> Option<&'a ExprStringLiteral> {
|
||||
if name.is_string_literal_expr() {
|
||||
return name.as_string_literal_expr();
|
||||
}
|
||||
|
||||
if let Some(name_expr) = name.as_name_expr() {
|
||||
let binding = semantic.binding(semantic.only_binding(name_expr)?);
|
||||
let value = find_binding_value(binding, semantic)?;
|
||||
if value.is_string_literal_expr() {
|
||||
return value.as_string_literal_expr();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
RUF046.py:7:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
6 | # Arguments are not checked
|
||||
7 | int(id())
|
||||
| ^^^^^^^^^ RUF046
|
||||
8 | int(len([]))
|
||||
9 | int(ord(foo))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | ### Safely fixable
|
||||
5 5 |
|
||||
6 6 | # Arguments are not checked
|
||||
7 |-int(id())
|
||||
7 |+id()
|
||||
8 8 | int(len([]))
|
||||
9 9 | int(ord(foo))
|
||||
10 10 | int(hash(foo, bar))
|
||||
|
||||
RUF046.py:8:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
6 | # Arguments are not checked
|
||||
7 | int(id())
|
||||
8 | int(len([]))
|
||||
| ^^^^^^^^^^^^ RUF046
|
||||
9 | int(ord(foo))
|
||||
10 | int(hash(foo, bar))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 |
|
||||
6 6 | # Arguments are not checked
|
||||
7 7 | int(id())
|
||||
8 |-int(len([]))
|
||||
8 |+len([])
|
||||
9 9 | int(ord(foo))
|
||||
10 10 | int(hash(foo, bar))
|
||||
11 11 | int(int(''))
|
||||
|
||||
RUF046.py:9:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
7 | int(id())
|
||||
8 | int(len([]))
|
||||
9 | int(ord(foo))
|
||||
| ^^^^^^^^^^^^^ RUF046
|
||||
10 | int(hash(foo, bar))
|
||||
11 | int(int(''))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | # Arguments are not checked
|
||||
7 7 | int(id())
|
||||
8 8 | int(len([]))
|
||||
9 |-int(ord(foo))
|
||||
9 |+ord(foo)
|
||||
10 10 | int(hash(foo, bar))
|
||||
11 11 | int(int(''))
|
||||
12 12 |
|
||||
|
||||
RUF046.py:10:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
8 | int(len([]))
|
||||
9 | int(ord(foo))
|
||||
10 | int(hash(foo, bar))
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF046
|
||||
11 | int(int(''))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | int(id())
|
||||
8 8 | int(len([]))
|
||||
9 9 | int(ord(foo))
|
||||
10 |-int(hash(foo, bar))
|
||||
10 |+hash(foo, bar)
|
||||
11 11 | int(int(''))
|
||||
12 12 |
|
||||
13 13 | int(math.comb())
|
||||
|
||||
RUF046.py:11:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
9 | int(ord(foo))
|
||||
10 | int(hash(foo, bar))
|
||||
11 | int(int(''))
|
||||
| ^^^^^^^^^^^^ RUF046
|
||||
12 |
|
||||
13 | int(math.comb())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | int(len([]))
|
||||
9 9 | int(ord(foo))
|
||||
10 10 | int(hash(foo, bar))
|
||||
11 |-int(int(''))
|
||||
11 |+int('')
|
||||
12 12 |
|
||||
13 13 | int(math.comb())
|
||||
14 14 | int(math.factorial())
|
||||
|
||||
RUF046.py:13:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
11 | int(int(''))
|
||||
12 |
|
||||
13 | int(math.comb())
|
||||
| ^^^^^^^^^^^^^^^^ RUF046
|
||||
14 | int(math.factorial())
|
||||
15 | int(math.gcd())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | int(hash(foo, bar))
|
||||
11 11 | int(int(''))
|
||||
12 12 |
|
||||
13 |-int(math.comb())
|
||||
13 |+math.comb()
|
||||
14 14 | int(math.factorial())
|
||||
15 15 | int(math.gcd())
|
||||
16 16 | int(math.lcm())
|
||||
|
||||
RUF046.py:14:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
13 | int(math.comb())
|
||||
14 | int(math.factorial())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ RUF046
|
||||
15 | int(math.gcd())
|
||||
16 | int(math.lcm())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | int(int(''))
|
||||
12 12 |
|
||||
13 13 | int(math.comb())
|
||||
14 |-int(math.factorial())
|
||||
14 |+math.factorial()
|
||||
15 15 | int(math.gcd())
|
||||
16 16 | int(math.lcm())
|
||||
17 17 | int(math.isqrt())
|
||||
|
||||
RUF046.py:15:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
13 | int(math.comb())
|
||||
14 | int(math.factorial())
|
||||
15 | int(math.gcd())
|
||||
| ^^^^^^^^^^^^^^^ RUF046
|
||||
16 | int(math.lcm())
|
||||
17 | int(math.isqrt())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 |
|
||||
13 13 | int(math.comb())
|
||||
14 14 | int(math.factorial())
|
||||
15 |-int(math.gcd())
|
||||
15 |+math.gcd()
|
||||
16 16 | int(math.lcm())
|
||||
17 17 | int(math.isqrt())
|
||||
18 18 | int(math.perm())
|
||||
|
||||
RUF046.py:16:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
14 | int(math.factorial())
|
||||
15 | int(math.gcd())
|
||||
16 | int(math.lcm())
|
||||
| ^^^^^^^^^^^^^^^ RUF046
|
||||
17 | int(math.isqrt())
|
||||
18 | int(math.perm())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | int(math.comb())
|
||||
14 14 | int(math.factorial())
|
||||
15 15 | int(math.gcd())
|
||||
16 |-int(math.lcm())
|
||||
16 |+math.lcm()
|
||||
17 17 | int(math.isqrt())
|
||||
18 18 | int(math.perm())
|
||||
19 19 |
|
||||
|
||||
RUF046.py:17:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
15 | int(math.gcd())
|
||||
16 | int(math.lcm())
|
||||
17 | int(math.isqrt())
|
||||
| ^^^^^^^^^^^^^^^^^ RUF046
|
||||
18 | int(math.perm())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | int(math.factorial())
|
||||
15 15 | int(math.gcd())
|
||||
16 16 | int(math.lcm())
|
||||
17 |-int(math.isqrt())
|
||||
17 |+math.isqrt()
|
||||
18 18 | int(math.perm())
|
||||
19 19 |
|
||||
20 20 |
|
||||
|
||||
RUF046.py:18:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
16 | int(math.lcm())
|
||||
17 | int(math.isqrt())
|
||||
18 | int(math.perm())
|
||||
| ^^^^^^^^^^^^^^^^ RUF046
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
15 15 | int(math.gcd())
|
||||
16 16 | int(math.lcm())
|
||||
17 17 | int(math.isqrt())
|
||||
18 |-int(math.perm())
|
||||
18 |+math.perm()
|
||||
19 19 |
|
||||
20 20 |
|
||||
21 21 | ### Unsafe
|
||||
|
||||
RUF046.py:23:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
21 | ### Unsafe
|
||||
22 |
|
||||
23 | int(math.ceil())
|
||||
| ^^^^^^^^^^^^^^^^ RUF046
|
||||
24 | int(math.floor())
|
||||
25 | int(math.trunc())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
20 20 |
|
||||
21 21 | ### Unsafe
|
||||
22 22 |
|
||||
23 |-int(math.ceil())
|
||||
23 |+math.ceil()
|
||||
24 24 | int(math.floor())
|
||||
25 25 | int(math.trunc())
|
||||
26 26 |
|
||||
|
||||
RUF046.py:24:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
23 | int(math.ceil())
|
||||
24 | int(math.floor())
|
||||
| ^^^^^^^^^^^^^^^^^ RUF046
|
||||
25 | int(math.trunc())
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
21 21 | ### Unsafe
|
||||
22 22 |
|
||||
23 23 | int(math.ceil())
|
||||
24 |-int(math.floor())
|
||||
24 |+math.floor()
|
||||
25 25 | int(math.trunc())
|
||||
26 26 |
|
||||
27 27 |
|
||||
|
||||
RUF046.py:25:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
23 | int(math.ceil())
|
||||
24 | int(math.floor())
|
||||
25 | int(math.trunc())
|
||||
| ^^^^^^^^^^^^^^^^^ RUF046
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
22 22 |
|
||||
23 23 | int(math.ceil())
|
||||
24 24 | int(math.floor())
|
||||
25 |-int(math.trunc())
|
||||
25 |+math.trunc()
|
||||
26 26 |
|
||||
27 27 |
|
||||
28 28 | ### `round()`
|
||||
|
||||
RUF046.py:31:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
30 | ## Errors
|
||||
31 | int(round(0))
|
||||
| ^^^^^^^^^^^^^ RUF046
|
||||
32 | int(round(0, 0))
|
||||
33 | int(round(0, None))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 | ### `round()`
|
||||
29 29 |
|
||||
30 30 | ## Errors
|
||||
31 |-int(round(0))
|
||||
31 |+round(0)
|
||||
32 32 | int(round(0, 0))
|
||||
33 33 | int(round(0, None))
|
||||
34 34 |
|
||||
|
||||
RUF046.py:32:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
30 | ## Errors
|
||||
31 | int(round(0))
|
||||
32 | int(round(0, 0))
|
||||
| ^^^^^^^^^^^^^^^^ RUF046
|
||||
33 | int(round(0, None))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
29 29 |
|
||||
30 30 | ## Errors
|
||||
31 31 | int(round(0))
|
||||
32 |-int(round(0, 0))
|
||||
32 |+round(0)
|
||||
33 33 | int(round(0, None))
|
||||
34 34 |
|
||||
35 35 | int(round(0.1))
|
||||
|
||||
RUF046.py:33:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
31 | int(round(0))
|
||||
32 | int(round(0, 0))
|
||||
33 | int(round(0, None))
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF046
|
||||
34 |
|
||||
35 | int(round(0.1))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Safe fix
|
||||
30 30 | ## Errors
|
||||
31 31 | int(round(0))
|
||||
32 32 | int(round(0, 0))
|
||||
33 |-int(round(0, None))
|
||||
33 |+round(0)
|
||||
34 34 |
|
||||
35 35 | int(round(0.1))
|
||||
36 36 | int(round(0.1, None))
|
||||
|
||||
RUF046.py:35:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
33 | int(round(0, None))
|
||||
34 |
|
||||
35 | int(round(0.1))
|
||||
| ^^^^^^^^^^^^^^^ RUF046
|
||||
36 | int(round(0.1, None))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
32 32 | int(round(0, 0))
|
||||
33 33 | int(round(0, None))
|
||||
34 34 |
|
||||
35 |-int(round(0.1))
|
||||
35 |+round(0.1)
|
||||
36 36 | int(round(0.1, None))
|
||||
37 37 |
|
||||
38 38 | # Argument type is not checked
|
||||
|
||||
RUF046.py:36:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
35 | int(round(0.1))
|
||||
36 | int(round(0.1, None))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ RUF046
|
||||
37 |
|
||||
38 | # Argument type is not checked
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
33 33 | int(round(0, None))
|
||||
34 34 |
|
||||
35 35 | int(round(0.1))
|
||||
36 |-int(round(0.1, None))
|
||||
36 |+round(0.1)
|
||||
37 37 |
|
||||
38 38 | # Argument type is not checked
|
||||
39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})()
|
||||
|
||||
RUF046.py:41:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})()
|
||||
40 |
|
||||
41 | int(round(foo))
|
||||
| ^^^^^^^^^^^^^^^ RUF046
|
||||
42 | int(round(foo, 0))
|
||||
43 | int(round(foo, None))
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
38 38 | # Argument type is not checked
|
||||
39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})()
|
||||
40 40 |
|
||||
41 |-int(round(foo))
|
||||
41 |+round(foo)
|
||||
42 42 | int(round(foo, 0))
|
||||
43 43 | int(round(foo, None))
|
||||
44 44 |
|
||||
|
||||
RUF046.py:43:1: RUF046 [*] Value being casted is already an integer
|
||||
|
|
||||
41 | int(round(foo))
|
||||
42 | int(round(foo, 0))
|
||||
43 | int(round(foo, None))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ RUF046
|
||||
44 |
|
||||
45 | ## No errors
|
||||
|
|
||||
= help: Remove unnecessary conversion to `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
40 40 |
|
||||
41 41 | int(round(foo))
|
||||
42 42 | int(round(foo, 0))
|
||||
43 |-int(round(foo, None))
|
||||
43 |+round(foo)
|
||||
44 44 |
|
||||
45 45 | ## No errors
|
||||
46 46 | int(round(0, 3.14))
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
RUF055.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
5 | # this should be replaced with s.replace("abc", "")
|
||||
6 | re.sub("abc", "", s)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
|
|
||||
= help: Replace with `s.replace("abc", "")`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | s = "str"
|
||||
4 4 |
|
||||
5 5 | # this should be replaced with s.replace("abc", "")
|
||||
6 |-re.sub("abc", "", s)
|
||||
6 |+s.replace("abc", "")
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 9 | # this example, adapted from https://docs.python.org/3/library/re.html#re.sub,
|
||||
|
||||
RUF055.py:22:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
20 | # this one should be replaced with s.startswith("abc") because the Match is
|
||||
21 | # used in an if context for its truth value
|
||||
22 | if re.match("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF055
|
||||
23 | pass
|
||||
24 | if m := re.match("abc", s): # this should *not* be replaced
|
||||
|
|
||||
= help: Replace with `s.startswith("abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 |
|
||||
20 20 | # this one should be replaced with s.startswith("abc") because the Match is
|
||||
21 21 | # used in an if context for its truth value
|
||||
22 |-if re.match("abc", s):
|
||||
22 |+if s.startswith("abc"):
|
||||
23 23 | pass
|
||||
24 24 | if m := re.match("abc", s): # this should *not* be replaced
|
||||
25 25 | pass
|
||||
|
||||
RUF055.py:29:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
28 | # this should be replaced with "abc" in s
|
||||
29 | if re.search("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
30 | pass
|
||||
31 | re.search("abc", s) # this should not be replaced
|
||||
|
|
||||
= help: Replace with `"abc" in s`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | re.match("abc", s) # this should not be replaced because match returns a Match
|
||||
27 27 |
|
||||
28 28 | # this should be replaced with "abc" in s
|
||||
29 |-if re.search("abc", s):
|
||||
29 |+if "abc" in s:
|
||||
30 30 | pass
|
||||
31 31 | re.search("abc", s) # this should not be replaced
|
||||
32 32 |
|
||||
|
||||
RUF055.py:34:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
33 | # this should be replaced with "abc" == s
|
||||
34 | if re.fullmatch("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
35 | pass
|
||||
36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
|
|
||||
= help: Replace with `"abc" == s`
|
||||
|
||||
ℹ Safe fix
|
||||
31 31 | re.search("abc", s) # this should not be replaced
|
||||
32 32 |
|
||||
33 33 | # this should be replaced with "abc" == s
|
||||
34 |-if re.fullmatch("abc", s):
|
||||
34 |+if "abc" == s:
|
||||
35 35 | pass
|
||||
36 36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
37 37 |
|
||||
|
||||
RUF055.py:39:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
38 | # this should be replaced with s.split("abc")
|
||||
39 | re.split("abc", s)
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF055
|
||||
40 |
|
||||
41 | # these currently should not be modified because the patterns contain regex
|
||||
|
|
||||
= help: Replace with `s.split("abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
36 36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
37 37 |
|
||||
38 38 | # this should be replaced with s.split("abc")
|
||||
39 |-re.split("abc", s)
|
||||
39 |+s.split("abc")
|
||||
40 40 |
|
||||
41 41 | # these currently should not be modified because the patterns contain regex
|
||||
42 42 | # metacharacters
|
||||
|
||||
RUF055.py:70:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
69 | # this should trigger an unsafe fix because of the presence of comments
|
||||
70 | / re.sub(
|
||||
71 | | # pattern
|
||||
72 | | "abc",
|
||||
73 | | # repl
|
||||
74 | | "",
|
||||
75 | | s, # string
|
||||
76 | | )
|
||||
| |_^ RUF055
|
||||
|
|
||||
= help: Replace with `s.replace("abc", "")`
|
||||
|
||||
ℹ Unsafe fix
|
||||
67 67 | re.split("abc", s, maxsplit=2)
|
||||
68 68 |
|
||||
69 69 | # this should trigger an unsafe fix because of the presence of comments
|
||||
70 |-re.sub(
|
||||
71 |- # pattern
|
||||
72 |- "abc",
|
||||
73 |- # repl
|
||||
74 |- "",
|
||||
75 |- s, # string
|
||||
76 |-)
|
||||
70 |+s.replace("abc", "")
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
5 | # this should be replaced with s.replace("abc", "")
|
||||
6 | re.sub("abc", "", s)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
|
|
||||
= help: Replace with `s.replace("abc", "")`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | s = "str"
|
||||
4 4 |
|
||||
5 5 | # this should be replaced with s.replace("abc", "")
|
||||
6 |-re.sub("abc", "", s)
|
||||
6 |+s.replace("abc", "")
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 9 | # this example, adapted from https://docs.python.org/3/library/re.html#re.sub,
|
||||
|
||||
RUF055_0.py:22:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
20 | # this one should be replaced with s.startswith("abc") because the Match is
|
||||
21 | # used in an if context for its truth value
|
||||
22 | if re.match("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF055
|
||||
23 | pass
|
||||
24 | if m := re.match("abc", s): # this should *not* be replaced
|
||||
|
|
||||
= help: Replace with `s.startswith("abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 |
|
||||
20 20 | # this one should be replaced with s.startswith("abc") because the Match is
|
||||
21 21 | # used in an if context for its truth value
|
||||
22 |-if re.match("abc", s):
|
||||
22 |+if s.startswith("abc"):
|
||||
23 23 | pass
|
||||
24 24 | if m := re.match("abc", s): # this should *not* be replaced
|
||||
25 25 | pass
|
||||
|
||||
RUF055_0.py:29:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
28 | # this should be replaced with "abc" in s
|
||||
29 | if re.search("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
30 | pass
|
||||
31 | re.search("abc", s) # this should not be replaced
|
||||
|
|
||||
= help: Replace with `"abc" in s`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | re.match("abc", s) # this should not be replaced because match returns a Match
|
||||
27 27 |
|
||||
28 28 | # this should be replaced with "abc" in s
|
||||
29 |-if re.search("abc", s):
|
||||
29 |+if "abc" in s:
|
||||
30 30 | pass
|
||||
31 31 | re.search("abc", s) # this should not be replaced
|
||||
32 32 |
|
||||
|
||||
RUF055_0.py:34:4: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
33 | # this should be replaced with "abc" == s
|
||||
34 | if re.fullmatch("abc", s):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
35 | pass
|
||||
36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
|
|
||||
= help: Replace with `"abc" == s`
|
||||
|
||||
ℹ Safe fix
|
||||
31 31 | re.search("abc", s) # this should not be replaced
|
||||
32 32 |
|
||||
33 33 | # this should be replaced with "abc" == s
|
||||
34 |-if re.fullmatch("abc", s):
|
||||
34 |+if "abc" == s:
|
||||
35 35 | pass
|
||||
36 36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
37 37 |
|
||||
|
||||
RUF055_0.py:39:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
38 | # this should be replaced with s.split("abc")
|
||||
39 | re.split("abc", s)
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF055
|
||||
40 |
|
||||
41 | # these currently should not be modified because the patterns contain regex
|
||||
|
|
||||
= help: Replace with `s.split("abc")`
|
||||
|
||||
ℹ Safe fix
|
||||
36 36 | re.fullmatch("abc", s) # this should not be replaced
|
||||
37 37 |
|
||||
38 38 | # this should be replaced with s.split("abc")
|
||||
39 |-re.split("abc", s)
|
||||
39 |+s.split("abc")
|
||||
40 40 |
|
||||
41 41 | # these currently should not be modified because the patterns contain regex
|
||||
42 42 | # metacharacters
|
||||
|
||||
RUF055_0.py:70:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
69 | # this should trigger an unsafe fix because of the presence of comments
|
||||
70 | / re.sub(
|
||||
71 | | # pattern
|
||||
72 | | "abc",
|
||||
73 | | # repl
|
||||
74 | | "",
|
||||
75 | | s, # string
|
||||
76 | | )
|
||||
| |_^ RUF055
|
||||
77 |
|
||||
78 | # A diagnostic should not be emitted for `sub` replacements with backreferences or
|
||||
|
|
||||
= help: Replace with `s.replace("abc", "")`
|
||||
|
||||
ℹ Unsafe fix
|
||||
67 67 | re.split("abc", s, maxsplit=2)
|
||||
68 68 |
|
||||
69 69 | # this should trigger an unsafe fix because of the presence of comments
|
||||
70 |-re.sub(
|
||||
71 |- # pattern
|
||||
72 |- "abc",
|
||||
73 |- # repl
|
||||
74 |- "",
|
||||
75 |- s, # string
|
||||
76 |-)
|
||||
70 |+s.replace("abc", "")
|
||||
77 71 |
|
||||
78 72 | # A diagnostic should not be emitted for `sub` replacements with backreferences or
|
||||
79 73 | # most other ASCII escapes
|
||||
|
||||
RUF055_0.py:88:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
86 | # *not* `some_string.replace("a", "\\n")`.
|
||||
87 | # We currently emit diagnostics for some of these without fixing them.
|
||||
88 | re.sub(r"a", "\n", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
89 | re.sub(r"a", r"\n", "a")
|
||||
90 | re.sub(r"a", "\a", "a")
|
||||
|
|
||||
= help: Replace with `"a".replace("a", "\n")`
|
||||
|
||||
ℹ Safe fix
|
||||
85 85 | # `re.sub(r"a", r"\n", some_string)` is fixed to `some_string.replace("a", "\n")`
|
||||
86 86 | # *not* `some_string.replace("a", "\\n")`.
|
||||
87 87 | # We currently emit diagnostics for some of these without fixing them.
|
||||
88 |-re.sub(r"a", "\n", "a")
|
||||
88 |+"a".replace("a", "\n")
|
||||
89 89 | re.sub(r"a", r"\n", "a")
|
||||
90 90 | re.sub(r"a", "\a", "a")
|
||||
91 91 | re.sub(r"a", r"\a", "a")
|
||||
|
||||
RUF055_0.py:89:1: RUF055 Plain string pattern passed to `re` function
|
||||
|
|
||||
87 | # We currently emit diagnostics for some of these without fixing them.
|
||||
88 | re.sub(r"a", "\n", "a")
|
||||
89 | re.sub(r"a", r"\n", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
90 | re.sub(r"a", "\a", "a")
|
||||
91 | re.sub(r"a", r"\a", "a")
|
||||
|
|
||||
|
||||
RUF055_0.py:90:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
88 | re.sub(r"a", "\n", "a")
|
||||
89 | re.sub(r"a", r"\n", "a")
|
||||
90 | re.sub(r"a", "\a", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
91 | re.sub(r"a", r"\a", "a")
|
||||
|
|
||||
= help: Replace with `"a".replace("a", "\x07")`
|
||||
|
||||
ℹ Safe fix
|
||||
87 87 | # We currently emit diagnostics for some of these without fixing them.
|
||||
88 88 | re.sub(r"a", "\n", "a")
|
||||
89 89 | re.sub(r"a", r"\n", "a")
|
||||
90 |-re.sub(r"a", "\a", "a")
|
||||
90 |+"a".replace("a", "\x07")
|
||||
91 91 | re.sub(r"a", r"\a", "a")
|
||||
92 92 |
|
||||
93 93 | re.sub(r"a", "\?", "a")
|
||||
|
||||
RUF055_0.py:91:1: RUF055 Plain string pattern passed to `re` function
|
||||
|
|
||||
89 | re.sub(r"a", r"\n", "a")
|
||||
90 | re.sub(r"a", "\a", "a")
|
||||
91 | re.sub(r"a", r"\a", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
92 |
|
||||
93 | re.sub(r"a", "\?", "a")
|
||||
|
|
||||
|
||||
RUF055_0.py:93:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
91 | re.sub(r"a", r"\a", "a")
|
||||
92 |
|
||||
93 | re.sub(r"a", "\?", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
94 | re.sub(r"a", r"\?", "a")
|
||||
|
|
||||
= help: Replace with `"a".replace("a", "\\?")`
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 | re.sub(r"a", "\a", "a")
|
||||
91 91 | re.sub(r"a", r"\a", "a")
|
||||
92 92 |
|
||||
93 |-re.sub(r"a", "\?", "a")
|
||||
93 |+"a".replace("a", "\\?")
|
||||
94 94 | re.sub(r"a", r"\?", "a")
|
||||
|
||||
RUF055_0.py:94:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
93 | re.sub(r"a", "\?", "a")
|
||||
94 | re.sub(r"a", r"\?", "a")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
|
|
||||
= help: Replace with `"a".replace("a", "\\?")`
|
||||
|
||||
ℹ Safe fix
|
||||
91 91 | re.sub(r"a", r"\a", "a")
|
||||
92 92 |
|
||||
93 93 | re.sub(r"a", "\?", "a")
|
||||
94 |-re.sub(r"a", r"\?", "a")
|
||||
94 |+"a".replace("a", "\\?")
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF055_1.py:9:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
7 | pat1 = "needle"
|
||||
8 |
|
||||
9 | re.sub(pat1, "", haystack)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
10 |
|
||||
11 | # aliases are not followed, so this one should not trigger the rule
|
||||
|
|
||||
= help: Replace with `haystack.replace(pat1, "")`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | pat1 = "needle"
|
||||
8 8 |
|
||||
9 |-re.sub(pat1, "", haystack)
|
||||
9 |+haystack.replace(pat1, "")
|
||||
10 10 |
|
||||
11 11 | # aliases are not followed, so this one should not trigger the rule
|
||||
12 12 | if pat4 := pat1:
|
||||
|
||||
RUF055_1.py:17:1: RUF055 [*] Plain string pattern passed to `re` function
|
||||
|
|
||||
15 | # also works for the `repl` argument in sub
|
||||
16 | repl = "new"
|
||||
17 | re.sub(r"abc", repl, haystack)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
||||
|
|
||||
= help: Replace with `haystack.replace("abc", repl)`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 |
|
||||
15 15 | # also works for the `repl` argument in sub
|
||||
16 16 | repl = "new"
|
||||
17 |-re.sub(r"abc", repl, haystack)
|
||||
17 |+haystack.replace("abc", repl)
|
||||
@@ -96,7 +96,7 @@ impl Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u64, if it can be represented as that data type.
|
||||
/// Return the [`Int`] as an usize, if it can be represented as that data type.
|
||||
pub fn as_usize(&self) -> Option<usize> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => usize::try_from(*small).ok(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
|
||||
stage: build
|
||||
interruptible: true
|
||||
image:
|
||||
name: ghcr.io/astral-sh/ruff:0.8.1-alpine
|
||||
name: ghcr.io/astral-sh/ruff:0.8.2-alpine
|
||||
before_script:
|
||||
- cd $CI_PROJECT_DIR
|
||||
- ruff --version
|
||||
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -133,7 +133,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.1
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -17,6 +17,9 @@ libfuzzer = ["libfuzzer-sys/link_libfuzzer"]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { path = "../crates/red_knot_python_semantic" }
|
||||
red_knot_vendored = { path = "../crates/red_knot_vendored" }
|
||||
ruff_db = { path = "../crates/ruff_db" }
|
||||
ruff_linter = { path = "../crates/ruff_linter" }
|
||||
ruff_python_ast = { path = "../crates/ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "../crates/ruff_python_codegen" }
|
||||
@@ -26,12 +29,18 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter"}
|
||||
ruff_text_size = { path = "../crates/ruff_text_size" }
|
||||
|
||||
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
|
||||
similar = { version = "2.5.0" }
|
||||
tracing = { version = "0.1.40" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "red_knot_check_invalid_syntax"
|
||||
path = "fuzz_targets/red_knot_check_invalid_syntax.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ruff_parse_simple"
|
||||
path = "fuzz_targets/ruff_parse_simple.rs"
|
||||
|
||||
@@ -74,6 +74,15 @@ Each fuzzer harness in [`fuzz_targets`](fuzz_targets) targets a different aspect
|
||||
them in different ways. While there is implementation-specific documentation in the source code
|
||||
itself, each harness is briefly described below.
|
||||
|
||||
### `red_knot_check_invalid_syntax`
|
||||
|
||||
This fuzz harness checks that the type checker (Red Knot) does not panic when checking a source
|
||||
file with invalid syntax. This rejects any corpus entries that is already valid Python code.
|
||||
Currently, this is limited to syntax errors that's produced by Ruff's Python parser which means
|
||||
that it does not cover all possible syntax errors (<https://github.com/astral-sh/ruff/issues/11934>).
|
||||
A possible workaround for now would be to bypass the parser and run the type checker on all inputs
|
||||
regardless of syntax errors.
|
||||
|
||||
### `ruff_parse_simple`
|
||||
|
||||
This fuzz harness does not perform any "smart" testing of Ruff; it merely checks that the parsing
|
||||
|
||||
1
fuzz/corpus/red_knot_check_invalid_syntax
Symbolic link
1
fuzz/corpus/red_knot_check_invalid_syntax
Symbolic link
@@ -0,0 +1 @@
|
||||
ruff_fix_validity
|
||||
143
fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs
Normal file
143
fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! Fuzzer harness that runs the type checker to catch for panics for source code containing
|
||||
//! syntax errors.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use libfuzzer_sys::{fuzz_target, Corpus};
|
||||
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonVersion, 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_parser::{parse_unchecked, Mode};
|
||||
|
||||
/// Database that can be used for testing.
|
||||
///
|
||||
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
||||
#[salsa::db]
|
||||
struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
static TEST_DB: OnceLock<Mutex<TestDb>> = OnceLock::new();
|
||||
|
||||
fn do_fuzz(case: &[u8]) -> Corpus {
|
||||
let Ok(code) = std::str::from_utf8(case) else {
|
||||
return Corpus::Reject;
|
||||
};
|
||||
|
||||
let parsed = parse_unchecked(code, Mode::Module);
|
||||
if parsed.is_valid() {
|
||||
return Corpus::Reject;
|
||||
}
|
||||
|
||||
let mut db = TEST_DB
|
||||
.get_or_init(|| Mutex::new(setup_db()))
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
for path in &["/src/a.py", "/src/a.pyi"] {
|
||||
db.write_file(path, code).unwrap();
|
||||
let file = system_path_to_file(&*db, path).unwrap();
|
||||
check_types(&*db, file);
|
||||
db.memory_file_system().remove_file(path).unwrap();
|
||||
file.sync(&mut *db);
|
||||
}
|
||||
|
||||
Corpus::Keep
|
||||
}
|
||||
|
||||
fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) });
|
||||
@@ -11,16 +11,32 @@ fi
|
||||
|
||||
if [ ! -d corpus/ruff_fix_validity ]; then
|
||||
mkdir -p corpus/ruff_fix_validity
|
||||
read -p "Would you like to build a corpus from a python source code dataset? (this will take a long time!) [Y/n] " -n 1 -r
|
||||
echo
|
||||
cd corpus/ruff_fix_validity
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz
|
||||
|
||||
(
|
||||
cd corpus/ruff_fix_validity
|
||||
|
||||
read -p "Would you like to build a corpus from a python source code dataset? (this will take a long time!) [Y/n] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz
|
||||
fi
|
||||
|
||||
# Build a smaller corpus in addition to the (optional) larger corpus
|
||||
curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.13.0.tar.gz' | tar xz
|
||||
cp -r "../../../crates/red_knot_workspace/resources/test/corpus" "red_knot_workspace"
|
||||
cp -r "../../../crates/ruff_linter/resources/test/fixtures" "ruff_linter"
|
||||
cp -r "../../../crates/ruff_python_formatter/resources/test/fixtures" "ruff_python_formatter"
|
||||
cp -r "../../../crates/ruff_python_parser/resources" "ruff_python_parser"
|
||||
|
||||
# Delete all non-Python files
|
||||
find . -type f -not -name "*.py" -delete
|
||||
)
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
cargo +nightly fuzz cmin ruff_fix_validity -- -timeout=5
|
||||
else
|
||||
cargo fuzz cmin -s none ruff_fix_validity -- -timeout=5
|
||||
fi
|
||||
curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz
|
||||
cp -r "../../../crates/ruff_linter/resources/test" .
|
||||
cd -
|
||||
cargo fuzz cmin -s none ruff_fix_validity -- -timeout=5
|
||||
fi
|
||||
|
||||
echo "Done! You are ready to fuzz."
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -3843,6 +3843,7 @@
|
||||
"RUF04",
|
||||
"RUF040",
|
||||
"RUF041",
|
||||
"RUF046",
|
||||
"RUF048",
|
||||
"RUF05",
|
||||
"RUF052",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "scripts"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user