Compare commits

...

18 Commits

Author SHA1 Message Date
Dhruv Manilawala
b0e26e6fc8 Bump version to 0.8.2 (#14789) 2024-12-05 18:06:35 +05:30
Dhruv Manilawala
e9941cd714 [red-knot] Move standalone expr inference to for non-name target (#14788)
## Summary

Ref: https://github.com/astral-sh/ruff/pull/14754#discussion_r1871040646

## Test Plan

Remove the TODO comment and update the mdtest.
2024-12-05 18:06:20 +05:30
Dhruv Manilawala
43bf1a8907 Add tests for "keyword as identifier" syntax errors (#14754)
## Summary

This is related to #13778, more specifically
https://github.com/astral-sh/ruff/issues/13778#issuecomment-2513556004.

This PR adds various test cases where a keyword is being where an
identifier is expected. The tests are to make sure that red knot doesn't
panic, raises the syntax error and the identifier is added to the symbol
table. The final part allows editor related features like renaming the
symbol.
2024-12-05 17:32:48 +05:30
InSync
fda8b1f884 [ruff] Unnecessary cast to int (RUF046) (#14697)
## Summary

Resolves #11412.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-05 10:30:06 +01:00
David Peter
2d3f557875 [red-knot] Fallback for typing._NoDefaultType (#14783)
## Summary

`typing_extensions` has a `>=3.13` re-export for the `typing.NoDefault`
singleton, but not for `typing._NoDefaultType`. This causes problems as
soon as we understand `sys.version_info` branches, so we explicity
switch to `typing._NoDefaultType` for Python 3.13 and later.

This is a part of #14759 that I thought might make sense to break out
and merge in isolation.

## Test Plan

New test that will become more meaningful with #12700

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-05 09:17:55 +01:00
David Peter
bd27bfab5d [red-knot] Unify setup_db() functions, add TestDb builder (#14777)
## Summary

- Instead of seven (more or less similar) `setup_db` functions, use just
one in a single central place.
- For every test that needs customization beyond that, offer a
`TestDbBuilder` that can control the Python target version, custom
typeshed, and pre-existing files.

The main motivation for this is that we're soon going to need
customization of the Python version, and I didn't feel like adding this
to each of the existing `setup_db` functions.
2024-12-04 21:36:54 +01:00
InSync
155d34bbb9 [red-knot] Infer precise types for len() calls (#14599)
## Summary

Resolves #14598.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-04 11:16:53 -08:00
Well404
04c887c8fc Fix references for async-busy-wait (#14775) 2024-12-04 18:05:49 +01:00
David Peter
af43bd4b0f [red-knot] Gradual forms do not participate in equivalence/subtyping (#14758)
## Summary

This changeset contains various improvements concerning non-fully-static
types and their relationships:

- Make sure that non-fully-static types do not participate in
equivalence or subtyping.
- Clarify what `Type::is_equivalent_to` actually implements.
- Introduce `Type::is_fully_static`
- New tests making sure that multiple `Any`/`Unknown`s inside unions and
intersections are collapsed.

closes #14524

## Test Plan

- Added new unit tests for union and intersection builder
- Added new unit tests for `Type::is_equivalent_to`
- Added new unit tests for `Type::is_subtype_of`
- Added new property test making sure that non-fully-static types do not
participate in subtyping
2024-12-04 17:11:25 +01:00
Harutaka Kawamura
614917769e Remove @ in pytest.mark.parametrize rule messages (#14770) 2024-12-04 16:01:10 +01:00
Douglas Creager
8b23086eac [red-knot] Add typing.Any as a spelling for the Any type (#14742)
We already had a representation for the Any type, which we would use
e.g. for expressions without type annotations. We now recognize
`typing.Any` as a way to refer to this type explicitly. Like other
special forms, this is tracked correctly through aliasing, and isn't
confused with local definitions that happen to have the same name.

Closes #14544
2024-12-04 09:56:36 -05:00
David Peter
948549fcdc [red-knot] Test: Hashable/Sized => A/B (#14769)
## Summary

Minor change that uses two plain classes `A` and `B` instead of
`typing.Sized` and `typing.Hashable`.

The motivation is twofold: I remember that I was confused when I first
saw this test. Was there anything specific to `Sized` and `Hashable`
that was relevant here? (there is, these classes are not overlapping;
and you can build a proper intersection from them; but that's true for
almost all non-builtin classes).

I now ran into another problem while working on #14758: `Sized` and
`Hashable` are protocols that we don't fully understand yet. This
causing some trouble when trying to infer whether these are fully-static
types or not.
2024-12-04 15:00:27 +01:00
David Salvisberg
e67f7f243d [flake8-type-checking] Expands TC006 docs to better explain itself (#14749)
Closes: #14676

I think the consensus generally was to keep the rule as-is, but expand
the docs.

## Summary

Expands the docs for TC006 with an explanation for why the type
expression is always quoted, including mention of another potential
benefit to this style.
2024-12-04 13:16:31 +00:00
Dylan
c617b2a48a [pycodestyle] Handle f-strings properly for invalid-escape-sequence (W605) (#14748)
When fixing an invalid escape sequence in an f-string, each f-string
element is analyzed for valid escape characters prior to creating the
diagnostic and fix. This allows us to safely prefix with `r` to create a
raw string if no valid escape characters were found anywhere in the
f-string, and otherwise insert backslashes.

This fixes a bug in the original implementation: each "f-string part"
was treated separately, so it was not possible to tell whether a valid
escape character was or would be used elsewhere in the f-string.

Progress towards #11491 but format specifiers are not handled in this
PR.
2024-12-04 06:59:14 -06:00
Dhruv Manilawala
1685d95ed2 [red-knot] Add fuzzer to catch panics for invalid syntax (#14678)
## Summary

This PR adds a fuzzer harness for red knot that runs the type checker on
source code that contains invalid syntax.

Additionally, this PR also updates the `init-fuzzer.sh` script to
increase the corpus size to:
* Include various crates that includes Python source code
* Use the 3.13 CPython source code

And, remove any non-Python files from the final corpus so that when the
fuzzer tries to minify the corpus, it doesn't produce files that only
contains documentation content as that's just noise.

## Test Plan

Run `./fuzz/init-fuzzer.sh`, say no to the large dataset.
Run the fuzzer with `cargo +night fuzz run red_knot_check_invalid_syntax
-- -timeout=5`
2024-12-04 14:36:58 +05:30
Dhruv Manilawala
575deb5d4d Check AIR001 from builtin or providers operators module (#14631)
## Summary

This PR makes changes to the `AIR001` rule as per
https://github.com/astral-sh/ruff/pull/14627#discussion_r1860212307.

Additionally,
* Avoid returning the `Diagnostic` and update the checker in the rule
logic for consistency
* Remove test case for different keyword position (I don't think it's
required here)

## Test Plan

Add test cases for multiple operators from various modules.
2024-12-04 13:30:47 +05:30
Wei Lee
edce559431 [airflow]: extend removed names (AIR302) (#14734) 2024-12-03 21:39:43 +00:00
Brent Westbrook
62e358e929 [ruff] Extend unnecessary-regular-expression to non-literal strings (RUF055) (#14679)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-03 15:17:20 +00:00
68 changed files with 2784 additions and 705 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.8.1"
version = "0.8.2"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.8.1"
version = "0.8.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

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

View File

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

View File

@@ -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'=}"

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

View File

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

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

View File

@@ -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(&[

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'=}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", "\\?")

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.8.1"
version = "0.8.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
ruff_fix_validity

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

View File

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

View File

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

@@ -3843,6 +3843,7 @@
"RUF04",
"RUF040",
"RUF041",
"RUF046",
"RUF048",
"RUF05",
"RUF052",

View File

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