Compare commits

..

34 Commits

Author SHA1 Message Date
Charlie Marsh
9ae4fb3b9f Add benches 2024-02-09 15:39:15 -05:00
Charlie Marsh
c67d68271d Use shared finder 2024-02-09 15:39:15 -05:00
Charlie Marsh
56b148bb43 Box other strings 2024-02-09 15:39:15 -05:00
Charlie Marsh
0a5a4f6d92 Remove unnecessary string cloning from the parser 2024-02-09 15:39:15 -05:00
Micha Reiser
00ef01d035 Update pyproject-toml to 0.9 (#9916) 2024-02-09 20:38:34 +00:00
Charlie Marsh
52ebfc9718 Respect duplicates when rewriting type aliases (#9905)
## Summary

If a generic appears multiple times on the right-hand side, we should
only include it once on the left-hand side when rewriting.

Closes https://github.com/astral-sh/ruff/issues/9904.
2024-02-09 14:02:41 +00:00
Hoël Bagard
12a91f4e90 Fix E30X panics on blank lines with trailing white spaces (#9907) 2024-02-09 14:00:26 +00:00
Mikko Leppänen
b4f2882b72 [pydocstyle-D405] Allow using parameters as a sub-section header (#9894)
## Summary

This review contains a fix for
[D405](https://docs.astral.sh/ruff/rules/capitalize-section-name/)
(capitalize-section-name)
The problem is that Ruff considers the sub-section header as a normal
section if it has the same name as some section name. For instance, a
function/method has an argument named "parameters". This only applies if
you use Numpy style docstring.

See: [ISSUE](https://github.com/astral-sh/ruff/issues/9806)

The following will not raise D405 after the fix:
```python  
def some_function(parameters: list[str]):
    """A function with a parameters parameter

    Parameters
    ----------

    parameters:
        A list of string parameters
    """
    ...
```


## Test Plan

```bash
cargo test
```

---------

Co-authored-by: Mikko Leppänen <mikko.leppanen@vaisala.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-08 21:54:32 -05:00
Charlie Marsh
49fe1b85f2 Reduce size of Expr from 80 to 64 bytes (#9900)
## Summary

This PR reduces the size of `Expr` from 80 to 64 bytes, by reducing the
sizes of...

- `ExprCall` from 72 to 56 bytes, by using boxed slices for `Arguments`.
- `ExprCompare` from 64 to 48 bytes, by using boxed slices for its
various vectors.

In testing, the parser gets a bit faster, and the linter benchmarks
improve quite a bit.
2024-02-09 02:53:13 +00:00
Micha Reiser
bd8123c0d8 Fix clippy unused variable warning (#9902) 2024-02-08 22:13:31 +00:00
Micha Reiser
49c5e715f9 Filter out test rules in RuleSelector JSON schema (#9901) 2024-02-08 21:06:51 +00:00
Micha Reiser
fe7d965334 Reduce Result<Tok, LexicalError> size by using Box<str> instead of String (#9885) 2024-02-08 20:36:22 +00:00
Hoël Bagard
9027169125 [pycodestyle] Add blank line(s) rules (E301, E302, E303, E304, E305, E306) (#9266)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-08 18:35:08 +00:00
Micha Reiser
688177ff6a Use Rust 1.76 (#9897) 2024-02-08 18:20:08 +00:00
trag1c
eb2784c495 Corrected Path symlink method name (PTH114) (#9896)
## Summary
Corrects mentions of `Path.is_link` to `Path.is_symlink` (the former
doesn't exist).

## Test Plan
```sh
python scripts/generate_mkdocs.py && mkdocs serve -f mkdocs.public.yml
```
2024-02-08 13:09:28 -05:00
Charlie Marsh
6fffde72e7 Use memchr for string lexing (#9888)
## Summary

On `main`, string lexing consists of walking through the string
character-by-character to search for the closing quote (with some
nuance: we also need to skip escaped characters, and error if we see
newlines in non-triple-quoted strings). This PR rewrites `lex_string` to
instead use `memchr` to search for the closing quote, which is
significantly faster. On my machine, at least, the `globals.py`
benchmark (which contains a lot of docstrings) gets 40% faster...

```text
lexer/numpy/globals.py  time:   [3.6410 µs 3.6496 µs 3.6585 µs]
                        thrpt:  [806.53 MiB/s 808.49 MiB/s 810.41 MiB/s]
                 change:
                        time:   [-40.413% -40.185% -39.984%] (p = 0.00 < 0.05)
                        thrpt:  [+66.623% +67.181% +67.822%]
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild
lexer/unicode/pypinyin.py
                        time:   [12.422 µs 12.445 µs 12.467 µs]
                        thrpt:  [337.03 MiB/s 337.65 MiB/s 338.27 MiB/s]
                 change:
                        time:   [-9.4213% -9.1930% -8.9586%] (p = 0.00 < 0.05)
                        thrpt:  [+9.8401% +10.124% +10.401%]
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) high mild
  2 (2.00%) high severe
lexer/pydantic/types.py time:   [107.45 µs 107.50 µs 107.56 µs]
                        thrpt:  [237.11 MiB/s 237.24 MiB/s 237.35 MiB/s]
                 change:
                        time:   [-4.0108% -3.7005% -3.3787%] (p = 0.00 < 0.05)
                        thrpt:  [+3.4968% +3.8427% +4.1784%]
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  2 (2.00%) high mild
  5 (5.00%) high severe
lexer/numpy/ctypeslib.py
                        time:   [46.123 µs 46.165 µs 46.208 µs]
                        thrpt:  [360.36 MiB/s 360.69 MiB/s 361.01 MiB/s]
                 change:
                        time:   [-19.313% -18.996% -18.710%] (p = 0.00 < 0.05)
                        thrpt:  [+23.016% +23.451% +23.935%]
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  3 (3.00%) low mild
  1 (1.00%) high mild
  4 (4.00%) high severe
lexer/large/dataset.py  time:   [231.07 µs 231.19 µs 231.33 µs]
                        thrpt:  [175.87 MiB/s 175.97 MiB/s 176.06 MiB/s]
                 change:
                        time:   [-2.0437% -1.7663% -1.4922%] (p = 0.00 < 0.05)
                        thrpt:  [+1.5148% +1.7981% +2.0864%]
                        Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
  5 (5.00%) high mild
  5 (5.00%) high severe
```
2024-02-08 17:23:06 +00:00
Jane Lewis
ad313b9089 RUF027 no longer has false negatives with string literals inside of method calls (#9865)
Fixes #9857.

## Summary

Statements like `logging.info("Today it is: {day}")` will no longer be
ignored by RUF027. As before, statements like `"Today it is:
{day}".format(day="Tuesday")` will continue to be ignored.

## Test Plan

The snapshot tests were expanded to include new cases. Additionally, the
snapshot tests have been split in two to separate positive cases from
negative cases.
2024-02-08 10:00:20 -05:00
Charlie Marsh
f76a3e8502 Detect mark_safe usages in decorators (#9887)
## Summary

Django's `mark_safe` can also be used as a decorator, so we should
detect usages of `@mark_safe` for the purpose of the relevant Bandit
rule.

Closes https://github.com/astral-sh/ruff/issues/9780.
2024-02-07 23:10:46 -05:00
Tom Kuson
ed07fa08bd Fix list formatting in documention (#9886)
## Summary

Adds a blank line to render the list correctly.

## Test Plan

Ocular inspection
2024-02-07 20:01:21 -05:00
Charlie Marsh
45937426c7 Fix blank-line docstring rules for module-level docstrings (#9878)
## Summary

Given:

```python
"""Make a summary line.

Note:
----
  Per the code comment the next two lines are blank. "// The first blank line is the line containing the closing
      triple quotes, so we need at least two."

"""
```

It turns out we excluded the line ending in `"""`, because it's empty
(unlike for functions, where it consists of the indent). This PR changes
the `following_lines` iterator to always include the trailing newline,
which gives us correct and consistent handling between function and
module-level docstrings.

Closes https://github.com/astral-sh/ruff/issues/9877.
2024-02-07 16:48:28 -05:00
Charlie Marsh
533dcfb114 Add a note regarding ignore-without-code (#9879)
Closes https://github.com/astral-sh/ruff/issues/9863.
2024-02-07 21:20:18 +00:00
Hugo van Kemenade
bc023f47a1 Fix typo in option name: output_format -> output-format (#9874) 2024-02-07 16:17:58 +00:00
Jack McIvor
aa38307415 Add more NPY002 violations (#9862) 2024-02-07 09:54:11 -05:00
Charlie Marsh
e9ddd4819a Make show-settings filters directory-agnostic (#9866)
Closes https://github.com/astral-sh/ruff/issues/9864.
2024-02-07 03:20:27 +00:00
Micha Reiser
fdb5eefb33 Improve trailing comma rule performance (#9867) 2024-02-06 23:04:36 +00:00
Charlie Marsh
daae28efc7 Respect async with in timeout-without-await (#9859)
Closes https://github.com/astral-sh/ruff/issues/9855.
2024-02-06 12:04:24 -05:00
Charlie Marsh
75553ab1c0 Remove ecosystem failures (#9854)
## Summary

These are kinda disruptive, I'd prefer to TODO unless someone is
interested in solving them ASAP.
2024-02-06 09:45:13 -05:00
Charlie Marsh
c34908f5ad Use memchr for tab-indentation detection (#9853)
## Summary

The benchmarks show a pretty consistent 1% speedup here for all-rules,
though not enough to trigger our threshold of course:

![Screenshot 2024-02-05 at 11 55
59 PM](https://github.com/astral-sh/ruff/assets/1309177/317dca3f-f25f-46f5-8ea8-894a1747d006)
2024-02-06 09:44:56 -05:00
Charlie Marsh
a662c2447c Ignore builtins when detecting missing f-strings (#9849)
## Summary

Reported on Discord: if the name maps to a builtin, it's not bound
locally, so is very unlikely to be intended as an f-string expression.
2024-02-05 23:49:56 -05:00
Seo Sanghyeon
df7fb95cbc Index multiline f-strings (#9837)
Fix #9777.
2024-02-05 21:25:33 -05:00
Adrian
83195a6030 ruff-ecosystem: Add indico/indico repo (#9850)
It's a pretty big codebase using lots of different stuff, so a good
candidate for finding obscure problems.

I didn't look more closely which options are used (I have the feeling
`--select ALL` is not implied, since I see you adding it via
`check_options` for certain entries but not for others), the repo itself
has a pretty large ruff.toml - but assuming ecosystem just cares about
differences between base and head of a PR, `ALL` most likely makes
sense.
2024-02-06 00:37:58 +00:00
Daniël van Noord
d31d09d7cd Add `--preview` to instruction for running newly added tests (#9846)
## Summary

This surprised me while working on adding a test. I thought about adding
an additional `note`, but how often is this incorrect? In general,
people reading the contributing guidelines probably want to enable this
flag and those who don't will know enough about the testing setup to
have their own commands/aliases.

## Test Plan

Ran CI on local fork and got an all green.
2024-02-05 19:33:22 -05:00
Tyler C Laprade, CFA
0f436b71f3 Typo in 0.2.1 changelog (#9847)
`refurn` -> `refurb`
2024-02-05 17:51:27 -05:00
Eero Vaher
cd5bcd815d Mention a related setting in C408 description (#9839)
#2977 added the `allow-dict-calls-with-keyword-arguments` configuration
option for the `unnecessary-collection-call (C408)` rule, but it did not
update the rule description.
2024-02-06 03:57:53 +05:30
193 changed files with 6300 additions and 1768 deletions

View File

@@ -117,10 +117,7 @@ jobs:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
run: cargo insta test --all --exclude ruff_dev --all-features --unreferenced reject
- name: "Run dev tests"
# e.g. generating the schema — these should not run with all features enabled
run: cargo insta test -p ruff_dev --unreferenced reject
run: cargo insta test --all --all-features --unreferenced reject
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:

View File

@@ -7,7 +7,7 @@ within a source file).
### Preview features
- \[`refurn`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### Formatter

View File

@@ -231,7 +231,7 @@ Once you've completed the code for the rule itself, you can define tests with th
For example, if you're adding a new rule named `E402`, you would run:
```shell
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
```
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that

47
Cargo.lock generated
View File

@@ -217,12 +217,12 @@ checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bstr"
version = "1.6.2"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
dependencies = [
"memchr",
"regex-automata 0.3.9",
"regex-automata 0.4.3",
"serde",
]
@@ -1569,18 +1569,6 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
[[package]]
name = "pep440_rs"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887f66cc62717ea72caac4f1eb4e6f392224da3ffff3f40ec13ab427802746d6"
dependencies = [
"lazy_static",
"regex",
"serde",
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.4.0"
@@ -1595,12 +1583,12 @@ dependencies = [
[[package]]
name = "pep508_rs"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2"
checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e"
dependencies = [
"once_cell",
"pep440_rs 0.3.12",
"pep440_rs",
"regex",
"serde",
"thiserror",
@@ -1780,12 +1768,12 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.8.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef61ae096a2f8c8b49eca360679dbc25f57c99145f6634b6bc18fedb1f9c6c30"
checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9"
dependencies = [
"indexmap",
"pep440_rs 0.4.0",
"pep440_rs",
"pep508_rs",
"serde",
"toml",
@@ -1933,12 +1921,6 @@ dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
[[package]]
name = "regex-automata"
version = "0.4.3"
@@ -2189,7 +2171,7 @@ dependencies = [
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs 0.4.0",
"pep440_rs",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -2277,7 +2259,6 @@ dependencies = [
"rustc-hash",
"serde",
"smallvec",
"static_assertions",
]
[[package]]
@@ -2355,16 +2336,22 @@ version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.1",
"bstr",
"codspeed-criterion-compat",
"criterion",
"insta",
"is-macro",
"itertools 0.12.1",
"lalrpop",
"lalrpop-util",
"memchr",
"mimalloc",
"once_cell",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash",
"static_assertions",
"tikv-jemallocator",
"tiny-keccak",
"unicode-ident",
"unicode_names2",
@@ -2494,7 +2481,7 @@ dependencies = [
"log",
"once_cell",
"path-absolutize",
"pep440_rs 0.4.0",
"pep440_rs",
"regex",
"ruff_cache",
"ruff_formatter",

View File

@@ -19,6 +19,7 @@ argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
@@ -65,7 +66,7 @@ pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.78" }
pyproject-toml = { version = "0.8.2" }
pyproject-toml = { version = "0.9.0" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }

View File

@@ -31,9 +31,9 @@ pub(crate) fn show_settings(
let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: {path:?}")?;
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?;
if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: {settings_path:?}")?;
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?;
}
write!(writer, "{settings}")?;

View File

@@ -4,25 +4,29 @@ use std::process::Command;
const BIN_NAME: &str = "ruff";
#[cfg(not(target_os = "windows"))]
const TEST_FILTERS: &[(&str, &str)] = &[
("\"[^\\*\"]*/pyproject.toml", "\"[BASEPATH]/pyproject.toml"),
("\".*/crates", "\"[BASEPATH]/crates"),
("\".*/\\.ruff_cache", "\"[BASEPATH]/.ruff_cache"),
("\".*/ruff\"", "\"[BASEPATH]\""),
];
#[cfg(target_os = "windows")]
const TEST_FILTERS: &[(&str, &str)] = &[
(r#""[^\*"]*\\pyproject.toml"#, "\"[BASEPATH]/pyproject.toml"),
(r#"".*\\crates"#, "\"[BASEPATH]/crates"),
(r#"".*\\\.ruff_cache"#, "\"[BASEPATH]/.ruff_cache"),
(r#"".*\\ruff""#, "\"[BASEPATH]\""),
(r#"\\+(\w\w|\s|")"#, "/$1"),
];
#[test]
fn display_default_settings() {
insta::with_settings!({ filters => TEST_FILTERS.to_vec() }, {
// Navigate from the crate directory to the workspace root.
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let base_path = base_path.to_string_lossy();
// Escape the backslashes for the regex.
let base_path = regex::escape(&base_path);
#[cfg(not(target_os = "windows"))]
let test_filters = &[(base_path.as_ref(), "[BASEPATH]")];
#[cfg(target_os = "windows")]
let test_filters = &[
(base_path.as_ref(), "[BASEPATH]"),
(r#"\\+(\w\w|\s|\.|")"#, "/$1"),
];
insta::with_settings!({ filters => test_filters.to_vec() }, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
});

View File

@@ -205,7 +205,9 @@ linter.external = []
linter.ignore_init_module_imports = false
linter.logger_objects = []
linter.namespace_packages = []
linter.src = ["[BASEPATH]"]
linter.src = [
"[BASEPATH]",
]
linter.tab_size = 4
linter.line_length = 88
linter.task_tags = [

View File

@@ -21,8 +21,7 @@ impl<'a> LineSuffixes<'a> {
/// Takes all the pending line suffixes.
pub(super) fn take_pending<'l>(
&'l mut self,
) -> impl Iterator<Item = LineSuffixEntry<'a>> + DoubleEndedIterator + 'l + ExactSizeIterator
{
) -> impl DoubleEndedIterator<Item = LineSuffixEntry<'a>> + 'l + ExactSizeIterator {
self.suffixes.drain(..)
}

View File

@@ -0,0 +1,22 @@
from django.utils.safestring import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'
from django.utils.html import mark_safe
def some_func():
return mark_safe('<script>alert("evil!")</script>')
@mark_safe
def some_func():
return '<script>alert("evil!")</script>'

View File

@@ -1,18 +1,27 @@
import trio
async def foo():
async def func():
with trio.fail_after():
...
async def foo():
async def func():
with trio.fail_at():
await ...
async def foo():
async def func():
with trio.move_on_after():
...
async def foo():
async def func():
with trio.move_at():
await ...
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -19,8 +19,11 @@ numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()
numpy.random.rand()
numpy.random.ranf()
numpy.random.sample()
numpy.random.randn()
numpy.random.randint()
numpy.random.random()
numpy.random.random_integers()
numpy.random.random_sample()
numpy.random.choice()
@@ -35,7 +38,6 @@ numpy.random.exponential()
numpy.random.f()
numpy.random.gamma()
numpy.random.geometric()
numpy.random.get_state()
numpy.random.gumbel()
numpy.random.hypergeometric()
numpy.random.laplace()

View File

@@ -0,0 +1,848 @@
"""Fixtures for the errors E301, E302, E303, E304, E305 and E306.
Since these errors are about new lines, each test starts with either "No error" or "# E30X".
Each test's end is signaled by a "# end" line.
There should be no E30X error outside of a test's bound.
"""
# No error
class Class:
pass
# end
# No error
class Class:
"""Docstring"""
def __init__(self) -> None:
pass
# end
# No error
def func():
pass
# end
# No error
# comment
class Class:
pass
# end
# No error
# comment
def func():
pass
# end
# no error
def foo():
pass
def bar():
pass
class Foo(object):
pass
class Bar(object):
pass
# end
# No error
class Class(object):
def func1():
pass
def func2():
pass
# end
# No error
class Class(object):
def func1():
pass
# comment
def func2():
pass
# end
# No error
class Class:
def func1():
pass
# comment
def func2():
pass
# This is a
# ... multi-line comment
def func3():
pass
# This is a
# ... multi-line comment
@decorator
class Class:
def func1():
pass
# comment
def func2():
pass
@property
def func3():
pass
# end
# No error
try:
from nonexistent import Bar
except ImportError:
class Bar(object):
"""This is a Bar replacement"""
# end
# No error
def with_feature(f):
"""Some decorator"""
wrapper = f
if has_this_feature(f):
def wrapper(*args):
call_feature(args[0])
return f(*args)
return wrapper
# end
# No error
try:
next
except NameError:
def next(iterator, default):
for item in iterator:
return item
return default
# end
# No error
def fn():
pass
class Foo():
"""Class Foo"""
def fn():
pass
# end
# No error
# comment
def c():
pass
# comment
def d():
pass
# This is a
# ... multi-line comment
# And this one is
# ... a second paragraph
# ... which spans on 3 lines
# Function `e` is below
# NOTE: Hey this is a testcase
def e():
pass
def fn():
print()
# comment
print()
print()
# Comment 1
# Comment 2
# Comment 3
def fn2():
pass
# end
# no error
if __name__ == '__main__':
foo()
# end
# no error
defaults = {}
defaults.update({})
# end
# no error
def foo(x):
classification = x
definitely = not classification
# end
# no error
def bar(): pass
def baz(): pass
# end
# no error
def foo():
def bar(): pass
def baz(): pass
# end
# no error
from typing import overload
from typing import Union
# end
# no error
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
# end
# no error
def f(x: Union[int, str]) -> Union[int, str]:
return x
# end
# no error
from typing import Protocol
class C(Protocol):
@property
def f(self) -> int: ...
@property
def g(self) -> str: ...
# end
# no error
def f(
a,
):
pass
# end
# no error
if True:
class Class:
"""Docstring"""
def function(self):
...
# end
# no error
if True:
def function(self):
...
# end
# no error
@decorator
# comment
@decorator
def function():
pass
# end
# no error
class Class:
def method(self):
if True:
def function():
pass
# end
# no error
@decorator
async def function(data: None) -> None:
...
# end
# no error
class Class:
def method():
"""docstring"""
# comment
def function():
pass
# end
# no error
try:
if True:
# comment
class Class:
pass
except:
pass
# end
# no error
def f():
def f():
pass
# end
# no error
class MyClass:
# comment
def method(self) -> None:
pass
# end
# no error
def function1():
# Comment
def function2():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
if (
cond1
and cond2
):
pass
#end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
async def function1():
await function2()
async with function3():
pass
# end
# no error
class Test:
async
def a(self): pass
# end
# no error
class Test:
def a():
pass
# wrongly indented comment
def b():
pass
# end
# no error
def test():
pass
# Wrongly indented comment
pass
# end
# E301
class Class(object):
def func1():
pass
def func2():
pass
# end
# E301
class Class:
def fn1():
pass
# comment
def fn2():
pass
# end
# E302
"""Main module."""
def fn():
pass
# end
# E302
import sys
def get_sys_path():
return sys.path
# end
# E302
def a():
pass
def b():
pass
# end
# E302
def a():
pass
# comment
def b():
pass
# end
# E302
def a():
pass
async def b():
pass
# end
# E302
async def x():
pass
async def x(y: int = 1):
pass
# end
# E302
def bar():
pass
def baz(): pass
# end
# E302
def bar(): pass
def baz():
pass
# end
# E302
def f():
pass
# comment
@decorator
def g():
pass
# end
# E302
class Test:
pass
def method1():
return 1
def method2():
return 22
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
def fn():
_ = None
# arbitrary comment
def inner(): # E306 not expected (pycodestyle detects E306)
pass
# end
# E303
print()
print()
# end
# E303:5:1
print()
# comment
print()
# end
# E303:5:5 E303:8:5
def a():
print()
# comment
# another comment
print()
# end
# E303
#!python
"""This class docstring comes on line 5.
It gives error E303: too many blank lines (3)
"""
# end
# E303
class Class:
def a(self):
pass
def b(self):
pass
# end
# E303
if True:
a = 1
a = 2
# end
# E303
class Test:
# comment
# another comment
def test(self): pass
# end
# E303
class Test:
def a(self):
pass
# wrongly indented comment
def b(self):
pass
# end
# E303
def fn():
pass
pass
# end
# E304
@decorator
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
def function():
pass
# end
# E304
@decorator
# comment E304 not expected
# second comment E304 not expected
def function():
pass
# end
# E305:7:1
def fn():
print()
# comment
# another comment
fn()
# end
# E305
class Class():
pass
# comment
# another comment
a = 1
# end
# E305:8:1
def fn():
print()
# comment
# another comment
try:
fn()
except Exception:
pass
# end
# E305:5:1
def a():
print()
# Two spaces before comments, too.
if a():
a()
# end
#: E305:8:1
# Example from https://github.com/PyCQA/pycodestyle/issues/400
import stuff
def main():
blah, blah
if __name__ == '__main__':
main()
# end
# E306:3:5
def a():
x = 1
def b():
pass
# end
#: E306:3:5
async def a():
x = 1
def b():
pass
# end
#: E306:3:5 E306:5:9
def a():
x = 2
def b():
x = 1
def c():
pass
# end
# E306:3:5 E306:6:5
def a():
x = 1
class C:
pass
x = 2
def b():
pass
# end
# E306
def foo():
def bar():
pass
def baz(): pass
# end
# E306:3:5
def foo():
def bar(): pass
def baz():
pass
# end
# E306
def a():
x = 2
@decorator
def b():
pass
# end
# E306
def a():
x = 2
@decorator
async def b():
pass
# end
# E306
def a():
x = 2
async def b():
pass
# end

View File

@@ -1,2 +1,15 @@
'''trailing whitespace
inside a multiline string'''
f'''trailing whitespace
inside a multiline f-string'''
# Trailing whitespace after `{`
f'abc {
1 + 2
}'
# Trailing whitespace after `2`
f'abc {
1 + 2
}'

View File

@@ -562,3 +562,46 @@ def titlecase_sub_section_header():
Returns:
"""
def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213
"""Test parameters and attributes sections are capitalized correctly.
Parameters
----------
parameters:
A list of string parameters
other_parameters:
A dictionary of string attributes
Other Parameters
----------
other_parameters:
A dictionary of string attributes
parameters:
A list of string parameters
"""
def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
"""Test that lower case subsection header is valid even if it has the same name as section kind.
Parameters:
----------
parameters:
A list of string parameters
value:
Some value
"""
def test_lowercase_sub_section_header_different_kind(returns: int):
"""Test that lower case subsection header is valid even if it is of a different kind.
Parameters
------------------
returns:
some value
"""

View File

@@ -46,3 +46,8 @@ x: typing.TypeAlias = list[T]
# OK
x: TypeAlias
x: int = 1
# Ensure that "T" appears only once in the type parameters for the modernized
# type alias.
T = typing.TypeVar["T"]
Decorator: TypeAlias = typing.Callable[[T], T]

View File

@@ -1,85 +0,0 @@
val = 2
def simple_cases():
a = 4
b = "{a}" # RUF027
c = "{a} {b} f'{val}' " # RUF027
def escaped_string():
a = 4
b = "escaped string: {{ brackets surround me }}" # RUF027
def raw_string():
a = 4
b = r"raw string with formatting: {a}" # RUF027
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
def print_name(name: str):
a = 4
print("Hello, {name}!") # RUF027
print("The test value we're using today is {a}") # RUF027
def do_nothing(a):
return a
def nested_funcs():
a = 4
print(do_nothing(do_nothing("{a}"))) # RUF027
def tripled_quoted():
a = 4
c = a
single_line = """ {a} """ # RUF027
# RUF027
multi_line = a = """b { # comment
c} d
"""
def single_quoted_multi_line():
a = 4
# RUF027
b = " {\
a} \
"
def implicit_concat():
a = 4
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
print(f"{a}" "{a}" f"{b}") # RUF027
def escaped_chars():
a = 4
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
def alternative_formatter(src, **kwargs):
src.format(**kwargs)
def format2(src, *args):
pass
# These should not cause an RUF027 message
def negative_cases():
a = 4
positive = False
"""{a}"""
"don't format: {a}"
c = """ {b} """
d = "bad variable: {invalid}"
e = "incorrect syntax: {}"
json = "{ positive: false }"
json2 = "{ 'positive': false }"
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a = 5)
formatted = "{a}".fmt(a = 7)
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a = 5)))
print(format(do_nothing("{a}"), a = 5))
print("{a}".to_upper())
print(do_nothing("{a}").format(a = "Test"))
print(do_nothing("{a}").format2(a))
a = 4
"always ignore this: {a}"
print("but don't ignore this: {val}") # RUF027

View File

@@ -0,0 +1,70 @@
val = 2
"always ignore this: {val}"
print("but don't ignore this: {val}") # RUF027
def simple_cases():
a = 4
b = "{a}" # RUF027
c = "{a} {b} f'{val}' " # RUF027
def escaped_string():
a = 4
b = "escaped string: {{ brackets surround me }}" # RUF027
def raw_string():
a = 4
b = r"raw string with formatting: {a}" # RUF027
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
def print_name(name: str):
a = 4
print("Hello, {name}!") # RUF027
print("The test value we're using today is {a}") # RUF027
def nested_funcs():
a = 4
print(do_nothing(do_nothing("{a}"))) # RUF027
def tripled_quoted():
a = 4
c = a
single_line = """ {a} """ # RUF027
# RUF027
multi_line = a = """b { # comment
c} d
"""
def single_quoted_multi_line():
a = 4
# RUF027
b = " {\
a} \
"
def implicit_concat():
a = 4
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
print(f"{a}" "{a}" f"{b}") # RUF027
def escaped_chars():
a = 4
b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
def method_calls():
value = {}
value.method = print_name
first = "Wendy"
last = "Appleseed"
value.method("{first} {last}") # RUF027

View File

@@ -0,0 +1,36 @@
def do_nothing(a):
return a
def alternative_formatter(src, **kwargs):
src.format(**kwargs)
def format2(src, *args):
pass
# These should not cause an RUF027 message
def negative_cases():
a = 4
positive = False
"""{a}"""
"don't format: {a}"
c = """ {b} """
d = "bad variable: {invalid}"
e = "incorrect syntax: {}"
f = "uses a builtin: {max}"
json = "{ positive: false }"
json2 = "{ 'positive': false }"
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a=5)
formatted = "{a}".fmt(a=7)
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a=5)))
print(format(do_nothing("{a}"), a=5))
print("{a}".to_upper())
print(do_nothing("{a}").format(a="Test"))
print(do_nothing("{a}").format2(a))
print(("{a}" "{c}").format(a=1, c=2))
print("{a}".attribute.chaining.call(a=2))
print("{a} {c}".format(a))

View File

@@ -247,6 +247,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::HardcodedPasswordDefault) {
flake8_bandit::rules::hardcoded_password_default(checker, parameters);
}
if checker.enabled(Rule::SuspiciousMarkSafeUsage) {
for decorator in decorator_list {
flake8_bandit::rules::suspicious_function_decorator(checker, decorator);
}
}
if checker.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}

View File

@@ -31,8 +31,8 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, Keyword,
MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -989,12 +989,7 @@ where
}
Expr::Call(ast::ExprCall {
func,
arguments:
Arguments {
args,
keywords,
range: _,
},
arguments,
range: _,
}) => {
self.visit_expr(func);
@@ -1037,7 +1032,7 @@ where
});
match callable {
Some(typing::Callable::Bool) => {
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_boolean_test(arg);
}
@@ -1046,7 +1041,7 @@ where
}
}
Some(typing::Callable::Cast) => {
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_type_definition(arg);
}
@@ -1055,7 +1050,7 @@ where
}
}
Some(typing::Callable::NewType) => {
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1064,21 +1059,21 @@ where
}
}
Some(typing::Callable::TypeVar) => {
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
for arg in args {
self.visit_type_definition(arg);
}
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword {
arg,
value,
range: _,
} = keyword;
if let Some(id) = arg {
if id == "bound" {
if id.as_str() == "bound" {
self.visit_type_definition(value);
} else {
self.visit_non_type_definition(value);
@@ -1088,7 +1083,7 @@ where
}
Some(typing::Callable::NamedTuple) => {
// Ex) NamedTuple("a", [("a", int)])
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1117,7 +1112,7 @@ where
}
}
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword { arg, value, .. } = keyword;
match (arg.as_ref(), value) {
// Ex) NamedTuple("a", **{"a": int})
@@ -1144,7 +1139,7 @@ where
}
Some(typing::Callable::TypedDict) => {
// Ex) TypedDict("a", {"a": int})
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_non_type_definition(arg);
}
@@ -1167,13 +1162,13 @@ where
}
// Ex) TypedDict("a", a=int)
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword { value, .. } = keyword;
self.visit_type_definition(value);
}
}
Some(typing::Callable::MypyExtension) => {
let mut args = args.iter();
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
// Ex) DefaultNamedArg(bool | None, name="some_prop_name")
self.visit_type_definition(arg);
@@ -1181,13 +1176,13 @@ where
for arg in args {
self.visit_non_type_definition(arg);
}
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword { value, .. } = keyword;
self.visit_non_type_definition(value);
}
} else {
// Ex) DefaultNamedArg(type="bool", name="some_prop_name")
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword {
value,
arg,
@@ -1205,10 +1200,10 @@ where
// If we're in a type definition, we need to treat the arguments to any
// other callables as non-type definitions (i.e., we don't want to treat
// any strings as deferred type definitions).
for arg in args {
for arg in arguments.args.iter() {
self.visit_non_type_definition(arg);
}
for keyword in keywords {
for keyword in arguments.keywords.iter() {
let Keyword { value, .. } = keyword;
self.visit_non_type_definition(value);
}

View File

@@ -1,3 +1,4 @@
use crate::line_width::IndentWidth;
use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
@@ -15,11 +16,11 @@ use crate::rules::pycodestyle::rules::logical_lines::{
use crate::settings::LinterSettings;
/// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size.
fn expand_indent(line: &str, settings: &LinterSettings) -> usize {
pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize {
let line = line.trim_end_matches(['\n', '\r']);
let mut indent = 0;
let tab_size = settings.tab_size.as_usize();
let tab_size = indent_width.as_usize();
for c in line.bytes() {
match c {
b'\t' => indent = (indent / tab_size) * tab_size + tab_size,
@@ -85,7 +86,7 @@ pub(crate) fn check_logical_lines(
TextRange::new(locator.line_start(first_token.start()), first_token.start())
};
let indent_level = expand_indent(locator.slice(range), settings);
let indent_level = expand_indent(locator.slice(range), settings.tab_size);
let indent_size = 4;

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
@@ -14,6 +15,7 @@ use ruff_source_file::Locator;
use crate::directives::TodoComment;
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::BlankLinesChecker;
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
@@ -21,17 +23,37 @@ use crate::rules::{
};
use crate::settings::LinterSettings;
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
settings: &LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
if settings.rules.any_enabled(&[
Rule::BlankLineBetweenMethods,
Rule::BlankLinesTopLevel,
Rule::TooManyBlankLines,
Rule::BlankLineAfterDecorator,
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
let mut blank_lines_checker = BlankLinesChecker::default();
blank_lines_checker.check_lines(
tokens,
locator,
stylist,
settings.tab_size,
&mut diagnostics,
);
}
if settings.rules.enabled(Rule::BlanketNOQA) {
pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator);
}
@@ -95,7 +117,7 @@ pub(crate) fn check_tokens(
}
if settings.rules.enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer);
}
if settings.rules.any_enabled(&[

View File

@@ -137,6 +137,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods),
(Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel),
(Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines),
(Pycodestyle, "E304") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineAfterDecorator),
(Pycodestyle, "E305") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass),
(Pycodestyle, "E306") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition),
(Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine),
(Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
(Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong),

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_source_file::{Line, UniversalNewlineIterator, UniversalNewlines};
use ruff_source_file::{Line, NewlineWithTrailingNewline, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
@@ -130,6 +130,34 @@ impl SectionKind {
Self::Yields => "Yields",
}
}
/// Returns `true` if a section can contain subsections, as in:
/// ```python
/// Yields
/// ------
/// int
/// Description of the anonymous integer return value.
/// ```
///
/// For NumPy, see: <https://numpydoc.readthedocs.io/en/latest/format.html>
///
/// For Google, see: <https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings>
pub(crate) fn has_subsections(self) -> bool {
matches!(
self,
Self::Args
| Self::Arguments
| Self::OtherArgs
| Self::OtherParameters
| Self::OtherParams
| Self::Parameters
| Self::Raises
| Self::Returns
| Self::SeeAlso
| Self::Warns
| Self::Yields
)
}
}
pub(crate) struct SectionContexts<'a> {
@@ -356,13 +384,16 @@ impl<'a> SectionContext<'a> {
pub(crate) fn previous_line(&self) -> Option<&'a str> {
let previous =
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
previous.universal_newlines().last().map(|l| l.as_str())
previous
.universal_newlines()
.last()
.map(|line| line.as_str())
}
/// Returns the lines belonging to this section after the summary line.
pub(crate) fn following_lines(&self) -> UniversalNewlineIterator<'a> {
pub(crate) fn following_lines(&self) -> NewlineWithTrailingNewline<'a> {
let lines = self.following_lines_str();
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
NewlineWithTrailingNewline::with_offset(lines, self.offset() + self.data.summary_full_end)
}
fn following_lines_str(&self) -> &'a str {
@@ -459,13 +490,54 @@ fn is_docstring_section(
// args: The arguments to the function.
// """
// ```
// Or `parameters` in:
// ```python
// def func(parameters: tuple[int]):
// """Toggle the gizmo.
//
// Parameters:
// -----
// parameters:
// The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
if section_kind.has_subsections() {
if let Some(previous_section) = previous_section {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
if section_kind.as_str() != verbatim {
return false;
// If the section is more deeply indented, assume it's a subsection, as in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
if previous_section.indent_size < indent_size {
if section_kind.as_str() != verbatim {
return false;
}
}
// If the section isn't underlined, and isn't title-cased, assume it's a subsection,
// as in:
// ```python
// def func(parameters: tuple[int]):
// """Toggle the gizmo.
//
// Parameters:
// -----
// parameters:
// The arguments to the function.
// """
// ```
if !next_line_is_underline && verbatim.chars().next().is_some_and(char::is_lowercase) {
if section_kind.as_str() != verbatim {
return false;
}
}
}
}

View File

@@ -109,6 +109,7 @@ pub fn check_path(
path,
locator,
indexer,
stylist,
settings,
source_type,
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),

View File

@@ -264,6 +264,11 @@ impl Rule {
| Rule::BadQuotesMultilineString
| Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::BlankLineAfterDecorator
| Rule::BlankLineBetweenMethods
| Rule::BlankLinesAfterFunctionOrClass
| Rule::BlankLinesBeforeNestedDefinition
| Rule::BlankLinesTopLevel
| Rule::CommentedOutCode
| Rule::EmptyComment
| Rule::ExtraneousParentheses
@@ -296,6 +301,7 @@ impl Rule {
| Rule::ShebangNotFirstLine
| Rule::SingleLineImplicitStringConcatenation
| Rule::TabIndentation
| Rule::TooManyBlankLines
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UselessSemicolon

View File

@@ -321,6 +321,16 @@ mod schema {
true
}
})
.filter(|_rule| {
// Filter out all test-only rules
#[cfg(feature = "test-rules")]
#[allow(clippy::used_underscore_binding)]
if _rule.starts_with("RUF9") {
return false;
}
true
})
.sorted()
.map(Value::String)
.collect(),

View File

@@ -46,6 +46,7 @@ mod tests {
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousMarkSafeUsage, Path::new("S308.py"))]
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]

View File

@@ -40,7 +40,9 @@ impl Violation for HardcodedBindAllInterfaces {
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
let is_bind_all_interface = match string {
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
&**value == "0.0.0.0"
}
StringLike::BytesLiteral(_) => return,
};

View File

@@ -3,7 +3,7 @@
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -848,7 +848,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
// Eval
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
["django", "utils", "safestring" | "html", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen (`urlopen`, `urlretrieve`, `Request`)
["urllib", "request", "urlopen" | "urlretrieve" | "Request"] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
@@ -901,3 +901,27 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
checker.diagnostics.push(diagnostic);
}
}
/// S308
pub(crate) fn suspicious_function_decorator(checker: &mut Checker, decorator: &Decorator) {
let Some(diagnostic_kind) = checker
.semantic()
.resolve_call_path(&decorator.expression)
.and_then(|call_path| {
match call_path.as_slice() {
// MarkSafe
["django", "utils", "safestring" | "html", "mark_safe"] => {
Some(SuspiciousMarkSafeUsage.into())
}
_ => None,
}
})
else {
return;
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, decorator.range());
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -0,0 +1,34 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S308.py:5:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
4 | def some_func():
5 | return mark_safe('<script>alert("evil!")</script>')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
S308.py:8:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
8 | @mark_safe
| ^^^^^^^^^^ S308
9 | def some_func():
10 | return '<script>alert("evil!")</script>'
|
S308.py:17:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
16 | def some_func():
17 | return mark_safe('<script>alert("evil!")</script>')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
S308.py:20:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
20 | @mark_safe
| ^^^^^^^^^^ S308
21 | def some_func():
22 | return '<script>alert("evil!")</script>'
|

View File

@@ -59,11 +59,11 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt {
})),
arguments: Arguments {
args: if let Some(msg) = msg {
vec![msg.clone()]
Box::from([msg.clone()])
} else {
vec![]
Box::from([])
},
keywords: vec![],
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -91,7 +91,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
return;
}
let [arg] = arguments.args.as_slice() else {
let [arg] = &*arguments.args else {
return;
};

View File

@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Node;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, ExprContext, Stmt};
use ruff_python_ast::{self as ast, Comprehension, Expr, ExprContext, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -126,18 +126,13 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
match expr {
Expr::Call(ast::ExprCall {
func,
arguments:
Arguments {
args,
keywords,
range: _,
},
arguments,
range: _,
}) => {
match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => {
if matches!(id.as_str(), "filter" | "reduce" | "map") {
for arg in args {
for arg in arguments.args.iter() {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
@@ -148,7 +143,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
if attr == "reduce" {
if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() {
if id == "functools" {
for arg in args {
for arg in arguments.args.iter() {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
@@ -160,7 +155,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
_ => {}
}
for keyword in keywords {
for keyword in arguments.keywords.iter() {
if keyword.arg.as_ref().is_some_and(|arg| arg == "key")
&& keyword.value.is_lambda_expr()
{

View File

@@ -114,7 +114,7 @@ fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords {
for keyword in keywords.iter() {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
return true;

View File

@@ -1,10 +1,8 @@
use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::{LexResult, Spanned};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -12,20 +10,20 @@ use ruff_text_size::{Ranged, TextRange};
/// Simplified token type.
#[derive(Copy, Clone, PartialEq, Eq)]
enum TokenType {
Irrelevant,
NonLogicalNewline,
Newline,
Comma,
OpeningBracket,
OpeningSquareBracket,
OpeningCurlyBracket,
ClosingBracket,
For,
Named,
Def,
Lambda,
Colon,
String,
Newline,
NonLogicalNewline,
OpeningBracket,
ClosingBracket,
OpeningSquareBracket,
Colon,
Comma,
OpeningCurlyBracket,
Def,
For,
Lambda,
Irrelevant,
}
/// Simplified token specialized for the task.
@@ -54,30 +52,30 @@ impl Token {
}
}
impl From<&Spanned> for Token {
fn from(spanned: &Spanned) -> Self {
let r#type = match &spanned.0 {
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
let r#type = match tok {
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
Tok::For => TokenType::For,
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Rpar => TokenType::ClosingBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Rsqb => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
Tok::Comma => TokenType::Comma,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rbrace => TokenType::ClosingBracket,
Tok::Def => TokenType::Def,
Tok::For => TokenType::For,
Tok::Lambda => TokenType::Lambda,
// Import treated like a function.
Tok::Import => TokenType::Named,
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Comma => TokenType::Comma,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rpar | Tok::Rsqb | Tok::Rbrace => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
_ => TokenType::Irrelevant,
};
Self {
range: spanned.1,
r#type,
}
#[allow(clippy::inconsistent_struct_constructor)]
Self { range, r#type }
}
}
@@ -237,10 +235,12 @@ pub(crate) fn trailing_commas(
indexer: &Indexer,
) {
let mut fstrings = 0u32;
let tokens = tokens
.iter()
.flatten()
.filter_map(|spanned @ (tok, tok_range)| match tok {
let tokens = tokens.iter().filter_map(|result| {
let Ok((tok, tok_range)) = result else {
return None;
};
match tok {
// Completely ignore comments -- they just interfere with the logic.
Tok::Comment(_) => None,
// F-strings are handled as `String` token type with the complete range
@@ -263,69 +263,30 @@ pub(crate) fn trailing_commas(
}
_ => {
if fstrings == 0 {
Some(Token::from(spanned))
Some(Token::from((tok, *tok_range)))
} else {
None
}
}
});
let tokens = [Token::irrelevant(), Token::irrelevant()]
.into_iter()
.chain(tokens);
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
let tokens = tokens.coalesce(|previous, current| {
if previous.r#type == TokenType::NonLogicalNewline
&& current.r#type == TokenType::NonLogicalNewline
{
Ok(previous)
} else {
Err((previous, current))
}
});
// The current nesting of the comma contexts.
let mut prev = Token::irrelevant();
let mut prev_prev = Token::irrelevant();
let mut stack = vec![Context::new(ContextType::No)];
for (prev_prev, prev, token) in tokens.tuple_windows() {
// Update the comma context stack.
match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
(TokenType::Named, TokenType::Def) => {
stack.push(Context::new(ContextType::FunctionParameters));
}
(TokenType::Named | TokenType::ClosingBracket, _) => {
stack.push(Context::new(ContextType::CallArguments));
}
_ => {
stack.push(Context::new(ContextType::Tuple));
}
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
stack.push(Context::new(ContextType::Subscript));
}
_ => {
stack.push(Context::new(ContextType::List));
}
},
TokenType::OpeningCurlyBracket => {
stack.push(Context::new(ContextType::Dict));
}
TokenType::Lambda => {
stack.push(Context::new(ContextType::LambdaParameters));
}
TokenType::For => {
let len = stack.len();
stack[len - 1] = Context::new(ContextType::No);
}
TokenType::Comma => {
let len = stack.len();
stack[len - 1].inc();
}
_ => {}
for token in tokens {
if prev.r#type == TokenType::NonLogicalNewline
&& token.r#type == TokenType::NonLogicalNewline
{
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
continue;
}
let context = &stack[stack.len() - 1];
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.r#type == TokenType::ClosingBracket
@@ -412,5 +373,47 @@ pub(crate) fn trailing_commas(
if pop_context && stack.len() > 1 {
stack.pop();
}
prev_prev = prev;
prev = token;
}
}
fn update_context(
token: Token,
prev: Token,
prev_prev: Token,
stack: &mut Vec<Context>,
) -> Context {
let new_context = match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
(TokenType::Named, TokenType::Def) => Context::new(ContextType::FunctionParameters),
(TokenType::Named | TokenType::ClosingBracket, _) => {
Context::new(ContextType::CallArguments)
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
},
TokenType::OpeningCurlyBracket => Context::new(ContextType::Dict),
TokenType::Lambda => Context::new(ContextType::LambdaParameters),
TokenType::For => {
let last = stack.last_mut().expect("Stack to never be empty");
*last = Context::new(ContextType::No);
return *last;
}
TokenType::Comma => {
let last = stack.last_mut().expect("Stack to never be empty");
last.inc();
return *last;
}
_ => return stack.last().copied().expect("Stack to never be empty"),
};
stack.push(new_context);
new_context
}

View File

@@ -36,6 +36,9 @@ use crate::rules::flake8_comprehensions::settings::Settings;
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
///
/// ## Options
/// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments`
#[violation]
pub struct UnnecessaryCollectionCall {
obj_type: String,

View File

@@ -88,7 +88,7 @@ fn is_nullable_field<'a>(value: &'a Expr, semantic: &'a SemanticModel) -> Option
let mut null_key = false;
let mut blank_key = false;
let mut unique_key = false;
for keyword in &call.arguments.keywords {
for keyword in call.arguments.keywords.iter() {
let Some(argument) = &keyword.arg else {
continue;
};

View File

@@ -113,7 +113,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "dict"]))
{
for keyword in keywords {
for keyword in keywords.iter() {
if let Some(attr) = &keyword.arg {
if is_reserved_attr(attr) {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -97,7 +97,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
continue;
}
let [arg] = args.as_slice() else {
let [arg] = &**args else {
continue;
};
@@ -188,8 +188,8 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
let node3 = Expr::Call(ast::ExprCall {
func: Box::new(node2),
arguments: Arguments {
args: vec![node],
keywords: vec![],
args: Box::from([node]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -59,7 +59,7 @@ impl Violation for UnnecessaryDictKwargs {
/// PIE804
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
let mut duplicate_keywords = None;
for keyword in &call.arguments.keywords {
for keyword in call.arguments.keywords.iter() {
// keyword is a spread operator (indicated by None).
if keyword.arg.is_some() {
continue;
@@ -145,7 +145,7 @@ fn duplicates(call: &ast::ExprCall) -> FxHashSet<&str> {
call.arguments.keywords.len(),
BuildHasherDefault::default(),
);
for keyword in &call.arguments.keywords {
for keyword in call.arguments.keywords.iter() {
if let Some(name) = &keyword.arg {
if !seen.insert(name.as_str()) {
duplicates.insert(name.as_str());

View File

@@ -60,7 +60,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
}
// Verify that the call has exactly two arguments (no `step`).
let [start, _] = call.arguments.args.as_slice() else {
let [start, _] = &*call.arguments.args else {
return;
};

View File

@@ -69,7 +69,7 @@ pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else {
let ([op], [_right]) = (&**ops, &**comparators) else {
return;
};

View File

@@ -101,7 +101,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else {
let ([op], [right]) = (&**ops, &**comparators) else {
return;
};

View File

@@ -129,7 +129,7 @@ pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) {
return;
};
let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else {
let ([op], [comparator]) = (&**ops, &**comparators) else {
return;
};

View File

@@ -411,7 +411,7 @@ fn to_pytest_raises_args<'a>(
) -> Option<Cow<'a, str>> {
let args = match attr {
"assertRaises" | "failUnlessRaises" => {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
match (&*arguments.args, &*arguments.keywords) {
// Ex) `assertRaises(Exception)`
([arg], []) => Cow::Borrowed(checker.locator().slice(arg)),
// Ex) `assertRaises(expected_exception=Exception)`
@@ -427,7 +427,7 @@ fn to_pytest_raises_args<'a>(
}
}
"assertRaisesRegex" | "assertRaisesRegexp" => {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
match (&*arguments.args, &*arguments.keywords) {
// Ex) `assertRaisesRegex(Exception, regex)`
([arg1, arg2], []) => Cow::Owned(format!(
"{}, match={}",

View File

@@ -257,15 +257,18 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
}
let node = Expr::from(ast::StringLiteral {
value: elts.iter().fold(String::new(), |mut acc, elt| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
if !acc.is_empty() {
acc.push(',');
value: elts
.iter()
.fold(String::new(), |mut acc, elt| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(value.to_str());
}
acc.push_str(value.to_str());
}
acc
}),
acc
})
.into_boxed_str(),
..ast::StringLiteral::default()
});
Some(generator.expr(&node))
@@ -327,7 +330,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string(),
value: (*name).to_string().into_boxed_str(),
..ast::StringLiteral::default()
})
})
@@ -360,7 +363,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string(),
value: (*name).to_string().into_boxed_str(),
..ast::StringLiteral::default()
})
})
@@ -635,17 +638,17 @@ pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) {
}) = &decorator.expression
{
if checker.enabled(Rule::PytestParametrizeNamesWrongType) {
if let [names, ..] = args.as_slice() {
if let [names, ..] = &**args {
check_names(checker, decorator, names);
}
}
if checker.enabled(Rule::PytestParametrizeValuesWrongType) {
if let [names, values, ..] = args.as_slice() {
if let [names, values, ..] = &**args {
check_values(checker, names, values);
}
}
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
if let [_, values, ..] = args.as_slice() {
if let [_, values, ..] = &**args {
check_duplicates(checker, values);
}
}

View File

@@ -173,8 +173,8 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt {
fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr {
Expr::Compare(ast::ExprCompare {
left: Box::new(left.clone()),
ops: vec![cmp_op],
comparators: vec![right.clone()],
ops: Box::from([cmp_op]),
comparators: Box::from([right.clone()]),
range: TextRange::default(),
})
}
@@ -390,8 +390,8 @@ impl UnittestAssert {
let node1 = ast::ExprCall {
func: Box::new(node.into()),
arguments: Arguments {
args: vec![(**obj).clone(), (**cls).clone()],
keywords: vec![],
args: Box::from([(**obj).clone(), (**cls).clone()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),
@@ -434,8 +434,8 @@ impl UnittestAssert {
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: vec![(**regex).clone(), (**text).clone()],
keywords: vec![],
args: Box::from([(**regex).clone(), (**text).clone()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -221,6 +221,7 @@ pub(crate) fn avoidable_escaped_quote(
Tok::FStringMiddle {
value: string_contents,
is_raw,
triple_quoted: _,
} if !is_raw => {
let Some(context) = fstrings.last_mut() else {
continue;
@@ -361,6 +362,7 @@ pub(crate) fn unnecessary_escaped_quote(
Tok::FStringMiddle {
value: string_contents,
is_raw,
triple_quoted: _,
} if !is_raw => {
let Some(context) = fstrings.last_mut() else {
continue;

View File

@@ -437,8 +437,8 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: vec![target.clone(), node.into()],
keywords: vec![],
args: Box::from([target.clone(), node.into()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),
@@ -480,13 +480,13 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> {
else {
return None;
};
if ops != &[CmpOp::Eq] {
if **ops != [CmpOp::Eq] {
return None;
}
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return None;
};
let [comparator] = comparators.as_slice() else {
let [comparator] = &**comparators else {
return None;
};
if !comparator.is_name_expr() {
@@ -551,8 +551,8 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
};
let node2 = ast::ExprCompare {
left: Box::new(node1.into()),
ops: vec![CmpOp::In],
comparators: vec![node.into()],
ops: Box::from([CmpOp::In]),
comparators: Box::from([node.into()]),
range: TextRange::default(),
};
let in_expr = node2.into();

View File

@@ -217,7 +217,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
slice.range(),
);
let node = ast::StringLiteral {
value: capital_env_var,
value: capital_env_var.into_boxed_str(),
unicode: env_var.is_unicode(),
..ast::StringLiteral::default()
};

View File

@@ -185,8 +185,8 @@ pub(crate) fn if_expr_with_true_false(
.into(),
),
arguments: Arguments {
args: vec![test.clone()],
keywords: vec![],
args: Box::from([test.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -176,7 +176,7 @@ pub(crate) fn negation_with_equal_op(
);
let node = ast::ExprCompare {
left: left.clone(),
ops: vec![CmpOp::NotEq],
ops: Box::from([CmpOp::NotEq]),
comparators: comparators.clone(),
range: TextRange::default(),
};
@@ -206,7 +206,7 @@ pub(crate) fn negation_with_not_equal_op(
else {
return;
};
if !matches!(&ops[..], [CmpOp::NotEq]) {
if !matches!(&**ops, [CmpOp::NotEq]) {
return;
}
if is_exception_check(checker.semantic().current_statement()) {
@@ -231,7 +231,7 @@ pub(crate) fn negation_with_not_equal_op(
);
let node = ast::ExprCompare {
left: left.clone(),
ops: vec![CmpOp::Eq],
ops: Box::from([CmpOp::Eq]),
comparators: comparators.clone(),
range: TextRange::default(),
};
@@ -279,8 +279,8 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o
let node1 = ast::ExprCall {
func: Box::new(node.into()),
arguments: Arguments {
args: vec![*operand.clone()],
keywords: vec![],
args: Box::from([*operand.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -253,8 +253,7 @@ fn is_main_check(expr: &Expr) -> bool {
{
if let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() {
if id == "__name__" {
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] =
comparators.as_slice()
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**comparators
{
if value == "__main__" {
return true;

View File

@@ -122,7 +122,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
else {
return;
};
let [test_dict] = test_dict.as_slice() else {
let [test_dict] = &**test_dict else {
return;
};
let (expected_var, expected_value, default_var, default_value) = match ops[..] {
@@ -176,8 +176,8 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
let node3 = ast::ExprCall {
func: Box::new(node2.into()),
arguments: Arguments {
args: vec![node1, node],
keywords: vec![],
args: Box::from([node1, node]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),
@@ -233,11 +233,11 @@ pub(crate) fn if_exp_instead_of_dict_get(
else {
return;
};
let [test_dict] = test_dict.as_slice() else {
let [test_dict] = &**test_dict else {
return;
};
let (body, default_value) = match ops.as_slice() {
let (body, default_value) = match &**ops {
[CmpOp::In] => (body, orelse),
[CmpOp::NotIn] => (orelse, body),
_ => {
@@ -276,8 +276,8 @@ pub(crate) fn if_exp_instead_of_dict_get(
let fixed_node = ast::ExprCall {
func: Box::new(dict_get_node.into()),
arguments: Arguments {
args: vec![dict_key_node, default_value_node],
keywords: vec![],
args: Box::from([dict_key_node, default_value_node]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -64,10 +64,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else {
return;
};
if ops != &[CmpOp::Eq] {
if **ops != [CmpOp::Eq] {
return;
}
let [expr] = comparators.as_slice() else {
let [expr] = &**comparators else {
return;
};
let Some(literal_expr) = expr.as_literal_expr() else {
@@ -127,10 +127,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return;
};
if id != target || ops != &[CmpOp::Eq] {
if id != target || **ops != [CmpOp::Eq] {
return;
}
let [expr] = comparators.as_slice() else {
let [expr] = &**comparators else {
return;
};
let Some(literal_expr) = expr.as_literal_expr() else {

View File

@@ -194,7 +194,7 @@ pub(crate) fn key_in_dict_comprehension(checker: &mut Checker, comprehension: &C
/// SIM118 in a comparison.
pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprCompare) {
let [op] = compare.ops.as_slice() else {
let [op] = &*compare.ops else {
return;
};
@@ -202,7 +202,7 @@ pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprComp
return;
}
let [right] = compare.comparators.as_slice() else {
let [right] = &*compare.comparators else {
return;
};

View File

@@ -161,8 +161,8 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let value_node = ast::ExprCall {
func: Box::new(func_node.into()),
arguments: Arguments {
args: vec![if_test.clone()],
keywords: vec![],
args: Box::from([if_test.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -140,7 +140,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
range: _,
}) = &loop_.test
{
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
if let ([op], [comparator]) = (&**ops, &**comparators) {
let op = match op {
CmpOp::Eq => CmpOp::NotEq,
CmpOp::NotEq => CmpOp::Eq,
@@ -155,8 +155,8 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
};
let node = ast::ExprCompare {
left: left.clone(),
ops: vec![op],
comparators: vec![comparator.clone()],
ops: Box::from([op]),
comparators: Box::from([comparator.clone()]),
range: TextRange::default(),
};
node.into()
@@ -391,8 +391,8 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
arguments: Arguments {
args: vec![node.into()],
keywords: vec![],
args: Box::from([node.into()]),
keywords: Box::from([]),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -3,24 +3,20 @@ source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
---
TRIO100.py:5:5: TRIO100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
4 | async def foo():
4 | async def func():
5 | with trio.fail_after():
| _____^
6 | | ...
| |___________^ TRIO100
7 |
8 | async def foo():
|
TRIO100.py:13:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
TRIO100.py:15:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
12 | async def foo():
13 | with trio.move_on_after():
14 | async def func():
15 | with trio.move_on_after():
| _____^
14 | | ...
16 | | ...
| |___________^ TRIO100
15 |
16 | async def foo():
|

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use crate::checkers::ast::Checker;
@@ -53,19 +52,15 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &E
return;
}
let Expr::Call(ExprCall {
arguments: Arguments { args, keywords, .. },
..
}) = expr
else {
let Expr::Call(ExprCall { arguments, .. }) = expr else {
return;
};
if !keywords.is_empty() {
if !arguments.keywords.is_empty() {
return;
}
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = args.as_slice() else {
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = &*arguments.args else {
return;
};

View File

@@ -610,7 +610,7 @@ impl Violation for OsPathIsfile {
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.is_link()` can improve readability over the `os`
/// methods such as `Path.is_symlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
@@ -627,11 +627,11 @@ impl Violation for OsPathIsfile {
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_link()
/// Path("docs").is_symlink()
/// ```
///
/// ## References
/// - [Python documentation: `Path.is_link`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_link)
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)

View File

@@ -15,7 +15,7 @@ fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement {
/// Convert a string to a [`ast::FStringElement::Literal`].
pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value: s.to_owned(),
value: s.to_string().into_boxed_str(),
range: TextRange::default(),
})
}
@@ -53,7 +53,7 @@ pub(super) fn to_f_string_element(expr: &Expr) -> Option<ast::FStringElement> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => {
Some(ast::FStringElement::Literal(ast::FStringLiteralElement {
value: value.to_string(),
value: value.to_string().into_boxed_str(),
range: *range,
}))
}

View File

@@ -72,7 +72,8 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
None
}
})
.join(joiner),
.join(joiner)
.into_boxed_str(),
..ast::StringLiteral::default()
};
return Some(node.into());
@@ -115,7 +116,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner:
if !keywords.is_empty() {
return;
}
let [arg] = args.as_slice() else {
let [arg] = &**args else {
return;
};

View File

@@ -80,8 +80,11 @@ pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) {
"set_state" |
// Simple random data
"rand" |
"ranf" |
"sample" |
"randn" |
"randint" |
"random" |
"random_integers" |
"random_sample" |
"choice" |

View File

@@ -45,7 +45,7 @@ NPY002.py:20:1: NPY002 Replace legacy `np.random.set_state` call with `np.random
20 | numpy.random.set_state()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
21 | numpy.random.rand()
22 | numpy.random.randn()
22 | numpy.random.ranf()
|
NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator`
@@ -54,444 +54,464 @@ NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Gene
20 | numpy.random.set_state()
21 | numpy.random.rand()
| ^^^^^^^^^^^^^^^^^ NPY002
22 | numpy.random.randn()
23 | numpy.random.randint()
22 | numpy.random.ranf()
23 | numpy.random.sample()
|
NPY002.py:22:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator`
NPY002.py:22:1: NPY002 Replace legacy `np.random.ranf` call with `np.random.Generator`
|
20 | numpy.random.set_state()
21 | numpy.random.rand()
22 | numpy.random.randn()
| ^^^^^^^^^^^^^^^^^^ NPY002
23 | numpy.random.randint()
24 | numpy.random.random_integers()
22 | numpy.random.ranf()
| ^^^^^^^^^^^^^^^^^ NPY002
23 | numpy.random.sample()
24 | numpy.random.randn()
|
NPY002.py:23:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator`
NPY002.py:23:1: NPY002 Replace legacy `np.random.sample` call with `np.random.Generator`
|
21 | numpy.random.rand()
22 | numpy.random.randn()
23 | numpy.random.randint()
22 | numpy.random.ranf()
23 | numpy.random.sample()
| ^^^^^^^^^^^^^^^^^^^ NPY002
24 | numpy.random.randn()
25 | numpy.random.randint()
|
NPY002.py:24:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator`
|
22 | numpy.random.ranf()
23 | numpy.random.sample()
24 | numpy.random.randn()
| ^^^^^^^^^^^^^^^^^^ NPY002
25 | numpy.random.randint()
26 | numpy.random.random()
|
NPY002.py:25:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator`
|
23 | numpy.random.sample()
24 | numpy.random.randn()
25 | numpy.random.randint()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
26 | numpy.random.random()
27 | numpy.random.random_integers()
|
NPY002.py:24:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator`
NPY002.py:26:1: NPY002 Replace legacy `np.random.random` call with `np.random.Generator`
|
22 | numpy.random.randn()
23 | numpy.random.randint()
24 | numpy.random.random_integers()
24 | numpy.random.randn()
25 | numpy.random.randint()
26 | numpy.random.random()
| ^^^^^^^^^^^^^^^^^^^ NPY002
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
|
NPY002.py:27:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator`
|
25 | numpy.random.randint()
26 | numpy.random.random()
27 | numpy.random.random_integers()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
25 | numpy.random.random_sample()
26 | numpy.random.choice()
28 | numpy.random.random_sample()
29 | numpy.random.choice()
|
NPY002.py:25:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator`
NPY002.py:28:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator`
|
23 | numpy.random.randint()
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
26 | numpy.random.random()
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
26 | numpy.random.choice()
27 | numpy.random.bytes()
29 | numpy.random.choice()
30 | numpy.random.bytes()
|
NPY002.py:26:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator`
NPY002.py:29:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator`
|
24 | numpy.random.random_integers()
25 | numpy.random.random_sample()
26 | numpy.random.choice()
27 | numpy.random.random_integers()
28 | numpy.random.random_sample()
29 | numpy.random.choice()
| ^^^^^^^^^^^^^^^^^^^ NPY002
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
|
NPY002.py:27:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator`
NPY002.py:30:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator`
|
25 | numpy.random.random_sample()
26 | numpy.random.choice()
27 | numpy.random.bytes()
28 | numpy.random.random_sample()
29 | numpy.random.choice()
30 | numpy.random.bytes()
| ^^^^^^^^^^^^^^^^^^ NPY002
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
|
NPY002.py:28:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator`
NPY002.py:31:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator`
|
26 | numpy.random.choice()
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
29 | numpy.random.choice()
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
29 | numpy.random.permutation()
30 | numpy.random.beta()
32 | numpy.random.permutation()
33 | numpy.random.beta()
|
NPY002.py:29:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator`
NPY002.py:32:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator`
|
27 | numpy.random.bytes()
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
30 | numpy.random.bytes()
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
30 | numpy.random.beta()
31 | numpy.random.binomial()
33 | numpy.random.beta()
34 | numpy.random.binomial()
|
NPY002.py:30:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator`
NPY002.py:33:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator`
|
28 | numpy.random.shuffle()
29 | numpy.random.permutation()
30 | numpy.random.beta()
31 | numpy.random.shuffle()
32 | numpy.random.permutation()
33 | numpy.random.beta()
| ^^^^^^^^^^^^^^^^^ NPY002
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
|
NPY002.py:31:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator`
NPY002.py:34:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator`
|
29 | numpy.random.permutation()
30 | numpy.random.beta()
31 | numpy.random.binomial()
32 | numpy.random.permutation()
33 | numpy.random.beta()
34 | numpy.random.binomial()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
|
NPY002.py:32:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator`
NPY002.py:35:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator`
|
30 | numpy.random.beta()
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
33 | numpy.random.beta()
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
|
NPY002.py:33:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator`
NPY002.py:36:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator`
|
31 | numpy.random.binomial()
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
34 | numpy.random.binomial()
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
34 | numpy.random.exponential()
35 | numpy.random.f()
37 | numpy.random.exponential()
38 | numpy.random.f()
|
NPY002.py:34:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator`
NPY002.py:37:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator`
|
32 | numpy.random.chisquare()
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
35 | numpy.random.chisquare()
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
35 | numpy.random.f()
36 | numpy.random.gamma()
38 | numpy.random.f()
39 | numpy.random.gamma()
|
NPY002.py:35:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator`
NPY002.py:38:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator`
|
33 | numpy.random.dirichlet()
34 | numpy.random.exponential()
35 | numpy.random.f()
36 | numpy.random.dirichlet()
37 | numpy.random.exponential()
38 | numpy.random.f()
| ^^^^^^^^^^^^^^ NPY002
36 | numpy.random.gamma()
37 | numpy.random.geometric()
39 | numpy.random.gamma()
40 | numpy.random.geometric()
|
NPY002.py:36:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator`
NPY002.py:39:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator`
|
34 | numpy.random.exponential()
35 | numpy.random.f()
36 | numpy.random.gamma()
37 | numpy.random.exponential()
38 | numpy.random.f()
39 | numpy.random.gamma()
| ^^^^^^^^^^^^^^^^^^ NPY002
37 | numpy.random.geometric()
38 | numpy.random.get_state()
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
|
NPY002.py:37:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator`
NPY002.py:40:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator`
|
35 | numpy.random.f()
36 | numpy.random.gamma()
37 | numpy.random.geometric()
38 | numpy.random.f()
39 | numpy.random.gamma()
40 | numpy.random.geometric()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
|
NPY002.py:38:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator`
NPY002.py:41:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator`
|
36 | numpy.random.gamma()
37 | numpy.random.geometric()
38 | numpy.random.get_state()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
|
NPY002.py:39:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator`
|
37 | numpy.random.geometric()
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
39 | numpy.random.gamma()
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
| ^^^^^^^^^^^^^^^^^^^ NPY002
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
|
NPY002.py:40:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator`
NPY002.py:42:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator`
|
38 | numpy.random.get_state()
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
40 | numpy.random.geometric()
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
41 | numpy.random.laplace()
42 | numpy.random.logistic()
43 | numpy.random.laplace()
44 | numpy.random.logistic()
|
NPY002.py:41:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator`
NPY002.py:43:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator`
|
39 | numpy.random.gumbel()
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
41 | numpy.random.gumbel()
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
|
NPY002.py:42:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator`
NPY002.py:44:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator`
|
40 | numpy.random.hypergeometric()
41 | numpy.random.laplace()
42 | numpy.random.logistic()
42 | numpy.random.hypergeometric()
43 | numpy.random.laplace()
44 | numpy.random.logistic()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
|
NPY002.py:43:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator`
NPY002.py:45:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator`
|
41 | numpy.random.laplace()
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
43 | numpy.random.laplace()
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
|
NPY002.py:44:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator`
NPY002.py:46:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator`
|
42 | numpy.random.logistic()
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
44 | numpy.random.logistic()
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
| ^^^^^^^^^^^^^^^^^^^^^^ NPY002
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
|
NPY002.py:45:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator`
NPY002.py:47:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator`
|
43 | numpy.random.lognormal()
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
45 | numpy.random.lognormal()
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
| ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
|
NPY002.py:46:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator`
NPY002.py:48:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator`
|
44 | numpy.random.logseries()
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
46 | numpy.random.logseries()
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
|
NPY002.py:47:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator`
NPY002.py:49:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator`
|
45 | numpy.random.multinomial()
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
47 | numpy.random.multinomial()
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
|
NPY002.py:48:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator`
NPY002.py:50:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator`
|
46 | numpy.random.multivariate_normal()
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
48 | numpy.random.multivariate_normal()
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
|
NPY002.py:49:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator`
NPY002.py:51:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator`
|
47 | numpy.random.negative_binomial()
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
49 | numpy.random.negative_binomial()
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
50 | numpy.random.normal()
51 | numpy.random.pareto()
52 | numpy.random.normal()
53 | numpy.random.pareto()
|
NPY002.py:50:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator`
NPY002.py:52:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator`
|
48 | numpy.random.noncentral_chisquare()
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
50 | numpy.random.noncentral_chisquare()
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
| ^^^^^^^^^^^^^^^^^^^ NPY002
51 | numpy.random.pareto()
52 | numpy.random.poisson()
53 | numpy.random.pareto()
54 | numpy.random.poisson()
|
NPY002.py:51:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator`
NPY002.py:53:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator`
|
49 | numpy.random.noncentral_f()
50 | numpy.random.normal()
51 | numpy.random.pareto()
51 | numpy.random.noncentral_f()
52 | numpy.random.normal()
53 | numpy.random.pareto()
| ^^^^^^^^^^^^^^^^^^^ NPY002
52 | numpy.random.poisson()
53 | numpy.random.power()
54 | numpy.random.poisson()
55 | numpy.random.power()
|
NPY002.py:52:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator`
NPY002.py:54:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator`
|
50 | numpy.random.normal()
51 | numpy.random.pareto()
52 | numpy.random.poisson()
52 | numpy.random.normal()
53 | numpy.random.pareto()
54 | numpy.random.poisson()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
53 | numpy.random.power()
54 | numpy.random.rayleigh()
55 | numpy.random.power()
56 | numpy.random.rayleigh()
|
NPY002.py:53:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator`
NPY002.py:55:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator`
|
51 | numpy.random.pareto()
52 | numpy.random.poisson()
53 | numpy.random.power()
53 | numpy.random.pareto()
54 | numpy.random.poisson()
55 | numpy.random.power()
| ^^^^^^^^^^^^^^^^^^ NPY002
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
|
NPY002.py:54:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator`
NPY002.py:56:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator`
|
52 | numpy.random.poisson()
53 | numpy.random.power()
54 | numpy.random.rayleigh()
54 | numpy.random.poisson()
55 | numpy.random.power()
56 | numpy.random.rayleigh()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
|
NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator`
NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator`
|
53 | numpy.random.power()
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
55 | numpy.random.power()
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
|
NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator`
NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator`
|
54 | numpy.random.rayleigh()
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
56 | numpy.random.rayleigh()
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
|
NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator`
NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator`
|
55 | numpy.random.standard_cauchy()
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
57 | numpy.random.standard_cauchy()
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
|
NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator`
NPY002.py:60:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator`
|
56 | numpy.random.standard_exponential()
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
58 | numpy.random.standard_exponential()
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
|
NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator`
NPY002.py:61:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator`
|
57 | numpy.random.standard_gamma()
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
59 | numpy.random.standard_gamma()
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
| ^^^^^^^^^^^^^^^^^^^^^^^ NPY002
60 | numpy.random.triangular()
61 | numpy.random.uniform()
62 | numpy.random.triangular()
63 | numpy.random.uniform()
|
NPY002.py:60:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator`
NPY002.py:62:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator`
|
58 | numpy.random.standard_normal()
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
60 | numpy.random.standard_normal()
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
| ^^^^^^^^^^^^^^^^^^^^^^^ NPY002
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
|
NPY002.py:61:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator`
NPY002.py:63:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator`
|
59 | numpy.random.standard_t()
60 | numpy.random.triangular()
61 | numpy.random.uniform()
61 | numpy.random.standard_t()
62 | numpy.random.triangular()
63 | numpy.random.uniform()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
62 | numpy.random.vonmises()
63 | numpy.random.wald()
64 | numpy.random.vonmises()
65 | numpy.random.wald()
|
NPY002.py:62:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator`
NPY002.py:64:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator`
|
60 | numpy.random.triangular()
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
62 | numpy.random.triangular()
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
| ^^^^^^^^^^^^^^^^^^^^^ NPY002
63 | numpy.random.wald()
64 | numpy.random.weibull()
65 | numpy.random.wald()
66 | numpy.random.weibull()
|
NPY002.py:63:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator`
NPY002.py:65:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator`
|
61 | numpy.random.uniform()
62 | numpy.random.vonmises()
63 | numpy.random.wald()
63 | numpy.random.uniform()
64 | numpy.random.vonmises()
65 | numpy.random.wald()
| ^^^^^^^^^^^^^^^^^ NPY002
64 | numpy.random.weibull()
65 | numpy.random.zipf()
66 | numpy.random.weibull()
67 | numpy.random.zipf()
|
NPY002.py:64:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator`
NPY002.py:66:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator`
|
62 | numpy.random.vonmises()
63 | numpy.random.wald()
64 | numpy.random.weibull()
64 | numpy.random.vonmises()
65 | numpy.random.wald()
66 | numpy.random.weibull()
| ^^^^^^^^^^^^^^^^^^^^ NPY002
65 | numpy.random.zipf()
67 | numpy.random.zipf()
|
NPY002.py:65:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator`
NPY002.py:67:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator`
|
63 | numpy.random.wald()
64 | numpy.random.weibull()
65 | numpy.random.zipf()
65 | numpy.random.wald()
66 | numpy.random.weibull()
67 | numpy.random.zipf()
| ^^^^^^^^^^^^^^^^^ NPY002
|

View File

@@ -109,7 +109,7 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo
return;
}
let [arg] = args.as_slice() else {
let [arg] = &**args else {
return;
};

View File

@@ -76,7 +76,7 @@ pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stm
return;
}
let [arg] = args.as_slice() else {
let [arg] = &**args else {
return;
};

View File

@@ -64,7 +64,7 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr, body: &[
return;
};
let [arg] = args.as_slice() else {
let [arg] = &**args else {
return;
};

View File

@@ -136,6 +136,13 @@ mod tests {
Path::new("E25.py")
)]
#[test_case(Rule::MissingWhitespaceAroundParameterEquals, Path::new("E25.py"))]
#[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))]
#[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))]
#[test_case(Rule::BlankLineAfterDecorator, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesAfterFunctionOrClass, Path::new("E30.py"))]
#[test_case(Rule::BlankLinesBeforeNestedDefinition, Path::new("E30.py"))]
fn logical(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,895 @@
use itertools::Itertools;
use std::cmp::Ordering;
use std::num::NonZeroU32;
use std::slice::Iter;
use ruff_diagnostics::AlwaysFixableViolation;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Edit;
use ruff_diagnostics::Fix;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::lexer::LexicalError;
use ruff_python_parser::Tok;
use ruff_python_parser::TokenKind;
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextRange;
use ruff_text_size::TextSize;
use crate::checkers::logical_lines::expand_indent;
use crate::line_width::IndentWidth;
use ruff_python_trivia::PythonWhitespace;
/// Number of blank lines around top level classes and functions.
const BLANK_LINES_TOP_LEVEL: u32 = 2;
/// Number of blank lines around methods and nested classes and functions.
const BLANK_LINES_METHOD_LEVEL: u32 = 1;
/// ## What it does
/// Checks for missing blank lines between methods of a class.
///
/// ## Why is this bad?
/// PEP 8 recommends exactly one blank line between methods of a class.
///
/// ## Example
/// ```python
/// class MyClass(object):
/// def func1():
/// pass
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class MyClass(object):
/// def func1():
/// pass
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
#[violation]
pub struct BlankLineBetweenMethods;
impl AlwaysFixableViolation for BlankLineBetweenMethods {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected {BLANK_LINES_METHOD_LEVEL:?} blank line, found 0")
}
fn fix_title(&self) -> String {
"Add missing blank line".to_string()
}
}
/// ## What it does
/// Checks for missing blank lines between top level functions and classes.
///
/// ## Why is this bad?
/// PEP 8 recommends exactly two blank lines between top level functions and classes.
///
/// ## Example
/// ```python
/// def func1():
/// pass
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def func1():
/// pass
///
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
#[violation]
pub struct BlankLinesTopLevel {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLinesTopLevel {
#[derive_message_formats]
fn message(&self) -> String {
let BlankLinesTopLevel {
actual_blank_lines: nb_blank_lines,
} = self;
format!("Expected {BLANK_LINES_TOP_LEVEL:?} blank lines, found {nb_blank_lines}")
}
fn fix_title(&self) -> String {
"Add missing blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for extraneous blank lines.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as follows:
/// - No more than two blank lines between top-level statements.
/// - No more than one blank line between non-top-level statements.
///
/// ## Example
/// ```python
/// def func1():
/// pass
///
///
///
/// def func2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def func1():
/// pass
///
///
/// def func2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
#[violation]
pub struct TooManyBlankLines {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for TooManyBlankLines {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyBlankLines {
actual_blank_lines: nb_blank_lines,
} = self;
format!("Too many blank lines ({nb_blank_lines})")
}
fn fix_title(&self) -> String {
"Remove extraneous blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for extraneous blank line(s) after function decorators.
///
/// ## Why is this bad?
/// There should be no blank lines between a decorator and the object it is decorating.
///
/// ## Example
/// ```python
/// class User(object):
///
/// @property
///
/// def name(self):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class User(object):
///
/// @property
/// def name(self):
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html)
#[violation]
pub struct BlankLineAfterDecorator {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLineAfterDecorator {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Blank lines found after function decorator ({lines})",
lines = self.actual_blank_lines
)
}
fn fix_title(&self) -> String {
"Remove extraneous blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for missing blank lines after the end of function or class.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as following:
/// - Two blank lines are expected between functions and classes
/// - One blank line is expected between methods of a class.
///
/// ## Example
/// ```python
/// class User(object):
/// pass
/// user = User()
/// ```
///
/// Use instead:
/// ```python
/// class User(object):
/// pass
///
///
/// user = User()
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
#[violation]
pub struct BlankLinesAfterFunctionOrClass {
actual_blank_lines: u32,
}
impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
#[derive_message_formats]
fn message(&self) -> String {
let BlankLinesAfterFunctionOrClass {
actual_blank_lines: blank_lines,
} = self;
format!("Expected 2 blank lines after class or function definition, found ({blank_lines})")
}
fn fix_title(&self) -> String {
"Add missing blank line(s)".to_string()
}
}
/// ## What it does
/// Checks for 1 blank line between nested function or class definitions.
///
/// ## Why is this bad?
/// PEP 8 recommends using blank lines as following:
/// - Two blank lines are expected between functions and classes
/// - One blank line is expected between methods of a class.
///
/// ## Example
/// ```python
/// def outer():
/// def inner():
/// pass
/// def inner2():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def outer():
/// def inner():
/// pass
///
/// def inner2():
/// pass
/// ```
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
#[violation]
pub struct BlankLinesBeforeNestedDefinition;
impl AlwaysFixableViolation for BlankLinesBeforeNestedDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected 1 blank line before a nested definition, found 0")
}
fn fix_title(&self) -> String {
"Add missing blank line".to_string()
}
}
#[derive(Debug)]
struct LogicalLineInfo {
kind: LogicalLineKind,
first_token_range: TextRange,
// The token's kind right before the newline ending the logical line.
last_token: TokenKind,
// The end of the logical line including the newline.
logical_line_end: TextSize,
// `true` if this is not a blank but only consists of a comment.
is_comment_only: bool,
/// `true` if the line is a string only (including trivia tokens) line, which is a docstring if coming right after a class/function definition.
is_docstring: bool,
/// The indentation length in columns. See [`expand_indent`] for the computation of the indent.
indent_length: usize,
/// The number of blank lines preceding the current line.
blank_lines: BlankLines,
/// The maximum number of consecutive blank lines between the current line
/// and the previous non-comment logical line.
/// One of its main uses is to allow a comments to directly precede or follow a class/function definition.
/// As such, `preceding_blank_lines` is used for rules that cannot trigger on comments (all rules except E303),
/// and `blank_lines` is used for the rule that can trigger on comments (E303).
preceding_blank_lines: BlankLines,
}
/// Iterator that processes tokens until a full logical line (or comment line) is "built".
/// It then returns characteristics of that logical line (see `LogicalLineInfo`).
struct LinePreprocessor<'a> {
tokens: Iter<'a, Result<(Tok, TextRange), LexicalError>>,
locator: &'a Locator<'a>,
indent_width: IndentWidth,
/// The start position of the next logical line.
line_start: TextSize,
/// Maximum number of consecutive blank lines between the current line and the previous non-comment logical line.
/// One of its main uses is to allow a comment to directly precede a class/function definition.
max_preceding_blank_lines: BlankLines,
}
impl<'a> LinePreprocessor<'a> {
fn new(
tokens: &'a [LexResult],
locator: &'a Locator,
indent_width: IndentWidth,
) -> LinePreprocessor<'a> {
LinePreprocessor {
tokens: tokens.iter(),
locator,
line_start: TextSize::new(0),
max_preceding_blank_lines: BlankLines::Zero,
indent_width,
}
}
}
impl<'a> Iterator for LinePreprocessor<'a> {
type Item = LogicalLineInfo;
fn next(&mut self) -> Option<LogicalLineInfo> {
let mut line_is_comment_only = true;
let mut is_docstring = false;
// Number of consecutive blank lines directly preceding this logical line.
let mut blank_lines = BlankLines::Zero;
let mut first_logical_line_token: Option<(LogicalLineKind, TextRange)> = None;
let mut last_token: TokenKind = TokenKind::EndOfFile;
let mut parens = 0u32;
while let Some(result) = self.tokens.next() {
let Ok((token, range)) = result else {
continue;
};
if matches!(token, Tok::Indent | Tok::Dedent) {
continue;
}
let token_kind = TokenKind::from_token(token);
let (logical_line_kind, first_token_range) = if let Some(first_token_range) =
first_logical_line_token
{
first_token_range
}
// At the start of the line...
else {
// An empty line
if token_kind == TokenKind::NonLogicalNewline {
blank_lines.add(*range);
self.line_start = range.end();
continue;
}
is_docstring = token_kind == TokenKind::String;
let logical_line_kind = match token_kind {
TokenKind::Class => LogicalLineKind::Class,
TokenKind::Comment => LogicalLineKind::Comment,
TokenKind::At => LogicalLineKind::Decorator,
TokenKind::Def => LogicalLineKind::Function,
// Lookahead to distinguish `async def` from `async with`.
TokenKind::Async
if matches!(self.tokens.as_slice().first(), Some(Ok((Tok::Def, _)))) =>
{
LogicalLineKind::Function
}
_ => LogicalLineKind::Other,
};
first_logical_line_token = Some((logical_line_kind, *range));
(logical_line_kind, *range)
};
if !token_kind.is_trivia() {
line_is_comment_only = false;
}
// A docstring line is composed only of the docstring (TokenKind::String) and trivia tokens.
// (If a comment follows a docstring, we still count the line as a docstring)
if token_kind != TokenKind::String && !token_kind.is_trivia() {
is_docstring = false;
}
match token_kind {
TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => {
parens = parens.saturating_add(1);
}
TokenKind::Rbrace | TokenKind::Rpar | TokenKind::Rsqb => {
parens = parens.saturating_sub(1);
}
TokenKind::Newline | TokenKind::NonLogicalNewline if parens == 0 => {
let indent_range = TextRange::new(self.line_start, first_token_range.start());
let indent_length =
expand_indent(self.locator.slice(indent_range), self.indent_width);
self.max_preceding_blank_lines =
self.max_preceding_blank_lines.max(blank_lines);
let logical_line = LogicalLineInfo {
kind: logical_line_kind,
first_token_range,
last_token,
logical_line_end: range.end(),
is_comment_only: line_is_comment_only,
is_docstring,
indent_length,
blank_lines,
preceding_blank_lines: self.max_preceding_blank_lines,
};
// Reset the blank lines after a non-comment only line.
if !line_is_comment_only {
self.max_preceding_blank_lines = BlankLines::Zero;
}
// Set the start for the next logical line.
self.line_start = range.end();
return Some(logical_line);
}
_ => {}
}
last_token = token_kind;
}
None
}
}
#[derive(Clone, Copy, Debug, Default)]
enum BlankLines {
/// No blank lines
#[default]
Zero,
/// One or more blank lines
Many { count: NonZeroU32, range: TextRange },
}
impl BlankLines {
fn add(&mut self, line_range: TextRange) {
match self {
BlankLines::Zero => {
*self = BlankLines::Many {
count: NonZeroU32::MIN,
range: line_range,
}
}
BlankLines::Many { count, range } => {
*count = count.saturating_add(1);
*range = TextRange::new(range.start(), line_range.end());
}
}
}
fn count(&self) -> u32 {
match self {
BlankLines::Zero => 0,
BlankLines::Many { count, .. } => count.get(),
}
}
fn range(&self) -> Option<TextRange> {
match self {
BlankLines::Zero => None,
BlankLines::Many { range, .. } => Some(*range),
}
}
}
impl PartialEq<u32> for BlankLines {
fn eq(&self, other: &u32) -> bool {
self.partial_cmp(other) == Some(Ordering::Equal)
}
}
impl PartialOrd<u32> for BlankLines {
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
self.count().partial_cmp(other)
}
}
impl PartialOrd for BlankLines {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BlankLines {
fn cmp(&self, other: &Self) -> Ordering {
self.count().cmp(&other.count())
}
}
impl PartialEq for BlankLines {
fn eq(&self, other: &Self) -> bool {
self.count() == other.count()
}
}
impl Eq for BlankLines {}
#[derive(Copy, Clone, Debug, Default)]
enum Follows {
#[default]
Other,
Decorator,
Def,
Docstring,
}
#[derive(Copy, Clone, Debug, Default)]
enum Status {
/// Stores the indent level where the nesting started.
Inside(usize),
/// This is used to rectify a Inside switched to a Outside because of a dedented comment.
CommentAfter(usize),
#[default]
Outside,
}
impl Status {
fn update(&mut self, line: &LogicalLineInfo) {
match *self {
Status::Inside(nesting_indent) => {
if line.indent_length <= nesting_indent {
if line.is_comment_only {
*self = Status::CommentAfter(nesting_indent);
} else {
*self = Status::Outside;
}
}
}
Status::CommentAfter(indent) => {
if !line.is_comment_only {
if line.indent_length > indent {
*self = Status::Inside(indent);
} else {
*self = Status::Outside;
}
}
}
Status::Outside => {
// Nothing to do
}
}
}
}
/// Contains variables used for the linting of blank lines.
#[derive(Debug, Default)]
pub(crate) struct BlankLinesChecker {
follows: Follows,
fn_status: Status,
class_status: Status,
/// First line that is not a comment.
is_not_first_logical_line: bool,
/// Used for the fix in case a comment separates two non-comment logical lines to make the comment "stick"
/// to the second line instead of the first.
last_non_comment_line_end: TextSize,
previous_unindented_line_kind: Option<LogicalLineKind>,
}
impl BlankLinesChecker {
/// E301, E302, E303, E304, E305, E306
pub(crate) fn check_lines(
&mut self,
tokens: &[LexResult],
locator: &Locator,
stylist: &Stylist,
indent_width: IndentWidth,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut prev_indent_length: Option<usize> = None;
let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width);
for logical_line in line_preprocessor {
self.check_line(
&logical_line,
prev_indent_length,
locator,
stylist,
diagnostics,
);
if !logical_line.is_comment_only {
prev_indent_length = Some(logical_line.indent_length);
}
}
}
#[allow(clippy::nonminimal_bool)]
fn check_line(
&mut self,
line: &LogicalLineInfo,
prev_indent_length: Option<usize>,
locator: &Locator,
stylist: &Stylist,
diagnostics: &mut Vec<Diagnostic>,
) {
self.class_status.update(line);
self.fn_status.update(line);
// Don't expect blank lines before the first non comment line.
if self.is_not_first_logical_line {
if line.preceding_blank_lines == 0
// Only applies to methods.
&& matches!(line.kind, LogicalLineKind::Function)
&& matches!(self.class_status, Status::Inside(_))
// The class/parent method's docstring can directly precede the def.
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Docstring | Follows::Decorator)
// Do not trigger when the def follows an if/while/etc...
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
{
// E301
let mut diagnostic =
Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().to_string(),
locator.line_start(self.last_non_comment_line_end),
)));
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Decorator)
// Allow groups of one-liners.
&& !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon))
// Only trigger on non-indented classes and functions (for example functions within an if are ignored)
&& line.indent_length == 0
// Only apply to functions or classes.
&& line.kind.is_top_level()
{
// E302
let mut diagnostic = Diagnostic::new(
BlankLinesTopLevel {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
blank_lines_range,
)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
locator.line_start(self.last_non_comment_line_end),
)));
}
diagnostics.push(diagnostic);
}
let expected_blank_lines = if line.indent_length > 0 {
BLANK_LINES_METHOD_LEVEL
} else {
BLANK_LINES_TOP_LEVEL
};
if line.blank_lines > expected_blank_lines {
// E303
let mut diagnostic = Diagnostic::new(
TooManyBlankLines {
actual_blank_lines: line.blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(expected_blank_lines as usize),
blank_lines_range,
)));
}
diagnostics.push(diagnostic);
}
if matches!(self.follows, Follows::Decorator)
&& !line.is_comment_only
&& line.preceding_blank_lines > 0
{
// E304
let mut diagnostic = Diagnostic::new(
BlankLineAfterDecorator {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
// Get all the lines between the last decorator line (included) and the current line (included).
// Then remove all blank lines.
let trivia_range = TextRange::new(
self.last_non_comment_line_end,
locator.line_start(line.first_token_range.start()),
);
let trivia_text = locator.slice(trivia_range);
let mut trivia_without_blank_lines = trivia_text
.universal_newlines()
.filter_map(|line| {
(!line.trim_whitespace().is_empty()).then_some(line.as_str())
})
.join(&stylist.line_ending());
let fix = if trivia_without_blank_lines.is_empty() {
Fix::safe_edit(Edit::range_deletion(trivia_range))
} else {
trivia_without_blank_lines.push_str(&stylist.line_ending());
Fix::safe_edit(Edit::range_replacement(
trivia_without_blank_lines,
trivia_range,
))
};
diagnostic.set_fix(fix);
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL
&& self
.previous_unindented_line_kind
.is_some_and(LogicalLineKind::is_top_level)
&& line.indent_length == 0
&& !line.is_comment_only
&& !line.kind.is_top_level()
{
// E305
let mut diagnostic = Diagnostic::new(
BlankLinesAfterFunctionOrClass {
actual_blank_lines: line.preceding_blank_lines.count(),
},
line.first_token_range,
);
if let Some(blank_lines_range) = line.blank_lines.range() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
blank_lines_range,
)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize),
locator.line_start(line.first_token_range.start()),
)));
}
diagnostics.push(diagnostic);
}
if line.preceding_blank_lines == 0
// Only apply to nested functions.
&& matches!(self.fn_status, Status::Inside(_))
&& line.kind.is_top_level()
// Allow following a decorator (if there is an error it will be triggered on the first decorator).
&& !matches!(self.follows, Follows::Decorator)
// The class's docstring can directly precede the first function.
&& !matches!(self.follows, Follows::Docstring)
// Do not trigger when the def/class follows an "indenting token" (if/while/etc...).
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
// Allow groups of one-liners.
&& !(matches!(self.follows, Follows::Def) && line.last_token != TokenKind::Colon)
{
// E306
let mut diagnostic =
Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
stylist.line_ending().to_string(),
locator.line_start(line.first_token_range.start()),
)));
diagnostics.push(diagnostic);
}
}
match line.kind {
LogicalLineKind::Class => {
if matches!(self.class_status, Status::Outside) {
self.class_status = Status::Inside(line.indent_length);
}
self.follows = Follows::Other;
}
LogicalLineKind::Decorator => {
self.follows = Follows::Decorator;
}
LogicalLineKind::Function => {
if matches!(self.fn_status, Status::Outside) {
self.fn_status = Status::Inside(line.indent_length);
}
self.follows = Follows::Def;
}
LogicalLineKind::Comment => {}
LogicalLineKind::Other => {
self.follows = Follows::Other;
}
}
if line.is_docstring {
self.follows = Follows::Docstring;
}
if !line.is_comment_only {
self.is_not_first_logical_line = true;
self.last_non_comment_line_end = line.logical_line_end;
if line.indent_length == 0 {
self.previous_unindented_line_kind = Some(line.kind);
}
}
}
}
#[derive(Copy, Clone, Debug)]
enum LogicalLineKind {
/// The clause header of a class definition
Class,
/// A decorator
Decorator,
/// The clause header of a function
Function,
/// A comment only line
Comment,
/// Any other statement or clause header
Other,
}
impl LogicalLineKind {
fn is_top_level(self) -> bool {
matches!(
self,
LogicalLineKind::Class | LogicalLineKind::Function | LogicalLineKind::Decorator
)
}
}

View File

@@ -67,14 +67,14 @@ pub(crate) fn invalid_escape_sequence(
token_range: TextRange,
) {
let (token_source_code, string_start_location) = match token {
Tok::FStringMiddle { value, is_raw } => {
Tok::FStringMiddle { value, is_raw, .. } => {
if *is_raw {
return;
}
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
return;
};
(value.as_str(), range.start())
(&**value, range.start())
}
Tok::String { kind, .. } => {
if kind.is_raw() {

View File

@@ -139,10 +139,10 @@ pub(crate) fn literal_comparisons(checker: &mut Checker, compare: &ast::ExprComp
// Check `left`.
let mut comparator = compare.left.as_ref();
let [op, ..] = compare.ops.as_slice() else {
let [op, ..] = &*compare.ops else {
return;
};
let [next, ..] = compare.comparators.as_slice() else {
let [next, ..] = &*compare.comparators else {
return;
};

View File

@@ -2,6 +2,7 @@ pub(crate) use ambiguous_class_name::*;
pub(crate) use ambiguous_function_name::*;
pub(crate) use ambiguous_variable_name::*;
pub(crate) use bare_except::*;
pub(crate) use blank_lines::*;
pub(crate) use compound_statements::*;
pub(crate) use doc_line_too_long::*;
pub(crate) use errors::*;
@@ -23,6 +24,7 @@ mod ambiguous_class_name;
mod ambiguous_function_name;
mod ambiguous_variable_name;
mod bare_except;
mod blank_lines;
mod compound_statements;
mod doc_line_too_long;
mod errors;

View File

@@ -90,7 +90,7 @@ pub(crate) fn not_tests(checker: &mut Checker, unary_op: &ast::ExprUnaryOp) {
return;
};
match ops.as_slice() {
match &**ops {
[CmpOp::In] => {
if checker.enabled(Rule::NotInTest) {
let mut diagnostic = Diagnostic::new(NotInTest, unary_op.operand.range());

View File

@@ -1,11 +1,8 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_trivia::leading_indentation;
use ruff_source_file::Locator;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
/// ## What it does
/// Checks for indentation that uses tabs.
@@ -48,44 +45,52 @@ impl Violation for TabIndentation {
/// W191
pub(crate) fn tab_indentation(
diagnostics: &mut Vec<Diagnostic>,
tokens: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) {
// Always check the first line for tab indentation as there's no newline
// token before it.
tab_indentation_at_line_start(diagnostics, locator, TextSize::default());
let contents = locator.contents().as_bytes();
let mut offset = 0;
while let Some(index) = memchr::memchr(b'\t', &contents[offset..]) {
// If we find a tab in the file, grab the entire line.
let range = locator.full_line_range(TextSize::try_from(offset + index).unwrap());
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::Newline | Tok::NonLogicalNewline) {
tab_indentation_at_line_start(diagnostics, locator, range.end());
// Determine whether the tab is part of the line's indentation.
if let Some(indent) = tab_indentation_at_line_start(range.start(), locator, indexer) {
diagnostics.push(Diagnostic::new(TabIndentation, indent));
}
// Advance to the next line.
offset = range.end().to_usize();
}
}
/// If a line includes tabs in its indentation, returns the range of the
/// indent.
fn tab_indentation_at_line_start(
line_start: TextSize,
locator: &Locator,
indexer: &Indexer,
) -> Option<TextRange> {
let mut contains_tab = false;
for (i, char) in locator.after(line_start).as_bytes().iter().enumerate() {
match char {
// If we find a tab character, report it as a violation.
b'\t' => {
contains_tab = true;
}
// If we find a space, continue.
b' ' | b'\x0C' => {}
// If we find a non-whitespace character, stop.
_ => {
if contains_tab {
let range = TextRange::at(line_start, TextSize::try_from(i).unwrap());
if !indexer.multiline_ranges().contains_range(range) {
return Some(range);
}
}
break;
}
}
}
// The lexer doesn't emit `Newline` / `NonLogicalNewline` for a line
// continuation character (`\`), so we need to manually check for tab
// indentation for lines that follow a line continuation character.
for continuation_line in indexer.continuation_line_starts() {
tab_indentation_at_line_start(
diagnostics,
locator,
locator.full_line_end(*continuation_line),
);
}
}
/// Checks for indentation that uses tabs for a line starting at
/// the given [`TextSize`].
fn tab_indentation_at_line_start(
diagnostics: &mut Vec<Diagnostic>,
locator: &Locator,
line_start: TextSize,
) {
let indent = leading_indentation(locator.after(line_start));
if indent.find('\t').is_some() {
diagnostics.push(Diagnostic::new(
TabIndentation,
TextRange::at(line_start, indent.text_len()),
));
}
None
}

View File

@@ -0,0 +1,44 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:453:5: E301 [*] Expected 1 blank line, found 0
|
451 | def func1():
452 | pass
453 | def func2():
| ^^^ E301
454 | pass
455 | # end
|
= help: Add missing blank line
Safe fix
450 450 |
451 451 | def func1():
452 452 | pass
453 |+
453 454 | def func2():
454 455 | pass
455 456 | # end
E30.py:464:5: E301 [*] Expected 1 blank line, found 0
|
462 | pass
463 | # comment
464 | def fn2():
| ^^^ E301
465 | pass
466 | # end
|
= help: Add missing blank line
Safe fix
460 460 |
461 461 | def fn1():
462 462 | pass
463 |+
463 464 | # comment
464 465 | def fn2():
465 466 | pass

View File

@@ -0,0 +1,187 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:471:1: E302 [*] Expected 2 blank lines, found 0
|
469 | # E302
470 | """Main module."""
471 | def fn():
| ^^^ E302
472 | pass
473 | # end
|
= help: Add missing blank line(s)
Safe fix
468 468 |
469 469 | # E302
470 470 | """Main module."""
471 |+
472 |+
471 473 | def fn():
472 474 | pass
473 475 | # end
E30.py:478:1: E302 [*] Expected 2 blank lines, found 0
|
476 | # E302
477 | import sys
478 | def get_sys_path():
| ^^^ E302
479 | return sys.path
480 | # end
|
= help: Add missing blank line(s)
Safe fix
475 475 |
476 476 | # E302
477 477 | import sys
478 |+
479 |+
478 480 | def get_sys_path():
479 481 | return sys.path
480 482 | # end
E30.py:487:1: E302 [*] Expected 2 blank lines, found 1
|
485 | pass
486 |
487 | def b():
| ^^^ E302
488 | pass
489 | # end
|
= help: Add missing blank line(s)
Safe fix
484 484 | def a():
485 485 | pass
486 486 |
487 |+
487 488 | def b():
488 489 | pass
489 490 | # end
E30.py:498:1: E302 [*] Expected 2 blank lines, found 1
|
496 | # comment
497 |
498 | def b():
| ^^^ E302
499 | pass
500 | # end
|
= help: Add missing blank line(s)
Safe fix
495 495 |
496 496 | # comment
497 497 |
498 |+
498 499 | def b():
499 500 | pass
500 501 | # end
E30.py:507:1: E302 [*] Expected 2 blank lines, found 1
|
505 | pass
506 |
507 | async def b():
| ^^^^^ E302
508 | pass
509 | # end
|
= help: Add missing blank line(s)
Safe fix
504 504 | def a():
505 505 | pass
506 506 |
507 |+
507 508 | async def b():
508 509 | pass
509 510 | # end
E30.py:516:1: E302 [*] Expected 2 blank lines, found 1
|
514 | pass
515 |
516 | async def x(y: int = 1):
| ^^^^^ E302
517 | pass
518 | # end
|
= help: Add missing blank line(s)
Safe fix
513 513 | async def x():
514 514 | pass
515 515 |
516 |+
516 517 | async def x(y: int = 1):
517 518 | pass
518 519 | # end
E30.py:524:1: E302 [*] Expected 2 blank lines, found 0
|
522 | def bar():
523 | pass
524 | def baz(): pass
| ^^^ E302
525 | # end
|
= help: Add missing blank line(s)
Safe fix
521 521 | # E302
522 522 | def bar():
523 523 | pass
524 |+
525 |+
524 526 | def baz(): pass
525 527 | # end
526 528 |
E30.py:530:1: E302 [*] Expected 2 blank lines, found 0
|
528 | # E302
529 | def bar(): pass
530 | def baz():
| ^^^ E302
531 | pass
532 | # end
|
= help: Add missing blank line(s)
Safe fix
527 527 |
528 528 | # E302
529 529 | def bar(): pass
530 |+
531 |+
530 532 | def baz():
531 533 | pass
532 534 | # end
E30.py:540:1: E302 [*] Expected 2 blank lines, found 1
|
539 | # comment
540 | @decorator
| ^ E302
541 | def g():
542 | pass
|
= help: Add missing blank line(s)
Safe fix
536 536 | def f():
537 537 | pass
538 538 |
539 |+
540 |+
539 541 | # comment
540 542 | @decorator
541 543 | def g():

View File

@@ -0,0 +1,250 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:555:2: E303 [*] Too many blank lines (2)
|
555 | def method2():
| ^^^ E303
556 | return 22
557 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
551 551 | def method1():
552 552 | return 1
553 553 |
554 |-
555 554 | def method2():
556 555 | return 22
557 556 | # end
E30.py:565:5: E303 [*] Too many blank lines (2)
|
565 | # arbitrary comment
| ^^^^^^^^^^^^^^^^^^^ E303
566 |
567 | def inner(): # E306 not expected (pycodestyle detects E306)
|
= help: Remove extraneous blank line(s)
Safe fix
561 561 | def fn():
562 562 | _ = None
563 563 |
564 |-
565 564 | # arbitrary comment
566 565 |
567 566 | def inner(): # E306 not expected (pycodestyle detects E306)
E30.py:577:5: E303 [*] Too many blank lines (2)
|
577 | # arbitrary comment
| ^^^^^^^^^^^^^^^^^^^ E303
578 | def inner(): # E306 not expected (pycodestyle detects E306)
579 | pass
|
= help: Remove extraneous blank line(s)
Safe fix
573 573 | def fn():
574 574 | _ = None
575 575 |
576 |-
577 576 | # arbitrary comment
578 577 | def inner(): # E306 not expected (pycodestyle detects E306)
579 578 | pass
E30.py:588:1: E303 [*] Too many blank lines (3)
|
588 | print()
| ^^^^^ E303
589 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
584 584 | print()
585 585 |
586 586 |
587 |-
588 587 | print()
589 588 | # end
590 589 |
E30.py:597:1: E303 [*] Too many blank lines (3)
|
597 | # comment
| ^^^^^^^^^ E303
598 |
599 | print()
|
= help: Remove extraneous blank line(s)
Safe fix
593 593 | print()
594 594 |
595 595 |
596 |-
597 596 | # comment
598 597 |
599 598 | print()
E30.py:608:5: E303 [*] Too many blank lines (2)
|
608 | # comment
| ^^^^^^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
604 604 | def a():
605 605 | print()
606 606 |
607 |-
608 607 | # comment
609 608 |
610 609 |
E30.py:611:5: E303 [*] Too many blank lines (2)
|
611 | # another comment
| ^^^^^^^^^^^^^^^^^ E303
612 |
613 | print()
|
= help: Remove extraneous blank line(s)
Safe fix
607 607 |
608 608 | # comment
609 609 |
610 |-
611 610 | # another comment
612 611 |
613 612 | print()
E30.py:622:1: E303 [*] Too many blank lines (3)
|
622 | / """This class docstring comes on line 5.
623 | | It gives error E303: too many blank lines (3)
624 | | """
| |___^ E303
625 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
618 618 | #!python
619 619 |
620 620 |
621 |-
622 621 | """This class docstring comes on line 5.
623 622 | It gives error E303: too many blank lines (3)
624 623 | """
E30.py:634:5: E303 [*] Too many blank lines (2)
|
634 | def b(self):
| ^^^ E303
635 | pass
636 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
630 630 | def a(self):
631 631 | pass
632 632 |
633 |-
634 633 | def b(self):
635 634 | pass
636 635 | # end
E30.py:644:5: E303 [*] Too many blank lines (2)
|
644 | a = 2
| ^ E303
645 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
640 640 | if True:
641 641 | a = 1
642 642 |
643 |-
644 643 | a = 2
645 644 | # end
646 645 |
E30.py:652:5: E303 [*] Too many blank lines (2)
|
652 | # comment
| ^^^^^^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
648 648 | # E303
649 649 | class Test:
650 650 |
651 |-
652 651 | # comment
653 652 |
654 653 |
E30.py:655:5: E303 [*] Too many blank lines (2)
|
655 | # another comment
| ^^^^^^^^^^^^^^^^^ E303
656 |
657 | def test(self): pass
|
= help: Remove extraneous blank line(s)
Safe fix
651 651 |
652 652 | # comment
653 653 |
654 |-
655 654 | # another comment
656 655 |
657 656 | def test(self): pass
E30.py:669:5: E303 [*] Too many blank lines (2)
|
669 | def b(self):
| ^^^ E303
670 | pass
671 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
665 665 |
666 666 | # wrongly indented comment
667 667 |
668 |-
669 668 | def b(self):
670 669 | pass
671 670 | # end
E30.py:679:5: E303 [*] Too many blank lines (2)
|
679 | pass
| ^^^^ E303
680 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
675 675 | def fn():
676 676 | pass
677 677 |
678 |-
679 678 | pass
680 679 | # end
681 680 |

View File

@@ -0,0 +1,65 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:686:1: E304 [*] Blank lines found after function decorator (1)
|
684 | @decorator
685 |
686 | def function():
| ^^^ E304
687 | pass
688 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
682 682 |
683 683 | # E304
684 684 | @decorator
685 |-
686 685 | def function():
687 686 | pass
688 687 | # end
E30.py:695:1: E304 [*] Blank lines found after function decorator (1)
|
694 | # comment E304 not expected
695 | def function():
| ^^^ E304
696 | pass
697 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
690 690 |
691 691 | # E304
692 692 | @decorator
693 |-
694 693 | # comment E304 not expected
695 694 | def function():
696 695 | pass
E30.py:707:1: E304 [*] Blank lines found after function decorator (2)
|
706 | # second comment E304 not expected
707 | def function():
| ^^^ E304
708 | pass
709 | # end
|
= help: Remove extraneous blank line(s)
Safe fix
699 699 |
700 700 | # E304
701 701 | @decorator
702 |-
703 702 | # comment E304 not expected
704 |-
705 |-
706 703 | # second comment E304 not expected
707 704 | def function():
708 705 | pass

View File

@@ -0,0 +1,102 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:719:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
718 | # another comment
719 | fn()
| ^^ E305
720 | # end
|
= help: Add missing blank line(s)
Safe fix
716 716 | # comment
717 717 |
718 718 | # another comment
719 |+
720 |+
719 721 | fn()
720 722 | # end
721 723 |
E30.py:730:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
729 | # another comment
730 | a = 1
| ^ E305
731 | # end
|
= help: Add missing blank line(s)
Safe fix
727 727 | # comment
728 728 |
729 729 | # another comment
730 |+
731 |+
730 732 | a = 1
731 733 | # end
732 734 |
E30.py:742:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
740 | # another comment
741 |
742 | try:
| ^^^ E305
743 | fn()
744 | except Exception:
|
= help: Add missing blank line(s)
Safe fix
739 739 |
740 740 | # another comment
741 741 |
742 |+
742 743 | try:
743 744 | fn()
744 745 | except Exception:
E30.py:754:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
753 | # Two spaces before comments, too.
754 | if a():
| ^^ E305
755 | a()
756 | # end
|
= help: Add missing blank line(s)
Safe fix
751 751 | print()
752 752 |
753 753 | # Two spaces before comments, too.
754 |+
755 |+
754 756 | if a():
755 757 | a()
756 758 | # end
E30.py:767:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
765 | blah, blah
766 |
767 | if __name__ == '__main__':
| ^^ E305
768 | main()
769 | # end
|
= help: Add missing blank line(s)
Safe fix
764 764 | def main():
765 765 | blah, blah
766 766 |
767 |+
767 768 | if __name__ == '__main__':
768 769 | main()
769 770 | # end

View File

@@ -0,0 +1,223 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.py:775:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
773 | def a():
774 | x = 1
775 | def b():
| ^^^ E306
776 | pass
777 | # end
|
= help: Add missing blank line
Safe fix
772 772 | # E306:3:5
773 773 | def a():
774 774 | x = 1
775 |+
775 776 | def b():
776 777 | pass
777 778 | # end
E30.py:783:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
781 | async def a():
782 | x = 1
783 | def b():
| ^^^ E306
784 | pass
785 | # end
|
= help: Add missing blank line
Safe fix
780 780 | #: E306:3:5
781 781 | async def a():
782 782 | x = 1
783 |+
783 784 | def b():
784 785 | pass
785 786 | # end
E30.py:791:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
789 | def a():
790 | x = 2
791 | def b():
| ^^^ E306
792 | x = 1
793 | def c():
|
= help: Add missing blank line
Safe fix
788 788 | #: E306:3:5 E306:5:9
789 789 | def a():
790 790 | x = 2
791 |+
791 792 | def b():
792 793 | x = 1
793 794 | def c():
E30.py:793:9: E306 [*] Expected 1 blank line before a nested definition, found 0
|
791 | def b():
792 | x = 1
793 | def c():
| ^^^ E306
794 | pass
795 | # end
|
= help: Add missing blank line
Safe fix
790 790 | x = 2
791 791 | def b():
792 792 | x = 1
793 |+
793 794 | def c():
794 795 | pass
795 796 | # end
E30.py:801:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
799 | def a():
800 | x = 1
801 | class C:
| ^^^^^ E306
802 | pass
803 | x = 2
|
= help: Add missing blank line
Safe fix
798 798 | # E306:3:5 E306:6:5
799 799 | def a():
800 800 | x = 1
801 |+
801 802 | class C:
802 803 | pass
803 804 | x = 2
E30.py:804:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
802 | pass
803 | x = 2
804 | def b():
| ^^^ E306
805 | pass
806 | # end
|
= help: Add missing blank line
Safe fix
801 801 | class C:
802 802 | pass
803 803 | x = 2
804 |+
804 805 | def b():
805 806 | pass
806 807 | # end
E30.py:813:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
811 | def bar():
812 | pass
813 | def baz(): pass
| ^^^ E306
814 | # end
|
= help: Add missing blank line
Safe fix
810 810 | def foo():
811 811 | def bar():
812 812 | pass
813 |+
813 814 | def baz(): pass
814 815 | # end
815 816 |
E30.py:820:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
818 | def foo():
819 | def bar(): pass
820 | def baz():
| ^^^ E306
821 | pass
822 | # end
|
= help: Add missing blank line
Safe fix
817 817 | # E306:3:5
818 818 | def foo():
819 819 | def bar(): pass
820 |+
820 821 | def baz():
821 822 | pass
822 823 | # end
E30.py:828:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
826 | def a():
827 | x = 2
828 | @decorator
| ^ E306
829 | def b():
830 | pass
|
= help: Add missing blank line
Safe fix
825 825 | # E306
826 826 | def a():
827 827 | x = 2
828 |+
828 829 | @decorator
829 830 | def b():
830 831 | pass
E30.py:837:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
835 | def a():
836 | x = 2
837 | @decorator
| ^ E306
838 | async def b():
839 | pass
|
= help: Add missing blank line
Safe fix
834 834 | # E306
835 835 | def a():
836 836 | x = 2
837 |+
837 838 | @decorator
838 839 | async def b():
839 840 | pass
E30.py:846:5: E306 [*] Expected 1 blank line before a nested definition, found 0
|
844 | def a():
845 | x = 2
846 | async def b():
| ^^^^^ E306
847 | pass
848 | # end
|
= help: Add missing blank line
Safe fix
843 843 | # E306
844 844 | def a():
845 845 | x = 2
846 |+
846 847 | async def b():
847 848 | pass
848 849 | # end

View File

@@ -13,5 +13,65 @@ W291.py:1:23: W291 [*] Trailing whitespace
1 |-'''trailing whitespace
1 |+'''trailing whitespace
2 2 | inside a multiline string'''
3 3 |
4 4 | f'''trailing whitespace
W291.py:4:24: W291 [*] Trailing whitespace
|
2 | inside a multiline string'''
3 |
4 | f'''trailing whitespace
| ^ W291
5 | inside a multiline f-string'''
|
= help: Remove trailing whitespace
Unsafe fix
1 1 | '''trailing whitespace
2 2 | inside a multiline string'''
3 3 |
4 |-f'''trailing whitespace
4 |+f'''trailing whitespace
5 5 | inside a multiline f-string'''
6 6 |
7 7 | # Trailing whitespace after `{`
W291.py:8:8: W291 [*] Trailing whitespace
|
7 | # Trailing whitespace after `{`
8 | f'abc {
| ^ W291
9 | 1 + 2
10 | }'
|
= help: Remove trailing whitespace
Safe fix
5 5 | inside a multiline f-string'''
6 6 |
7 7 | # Trailing whitespace after `{`
8 |-f'abc {
8 |+f'abc {
9 9 | 1 + 2
10 10 | }'
11 11 |
W291.py:14:10: W291 [*] Trailing whitespace
|
12 | # Trailing whitespace after `2`
13 | f'abc {
14 | 1 + 2
| ^ W291
15 | }'
|
= help: Remove trailing whitespace
Safe fix
11 11 |
12 12 | # Trailing whitespace after `2`
13 13 | f'abc {
14 |- 1 + 2
14 |+ 1 + 2
15 15 | }'

View File

@@ -1634,12 +1634,13 @@ fn common_section(
let line_end = checker.stylist().line_ending().as_str();
if let Some(next) = next {
if context
.following_lines()
.last()
.map_or(true, |line| !line.trim().is_empty())
{
if checker.enabled(Rule::NoBlankLineAfterSection) {
if checker.enabled(Rule::NoBlankLineAfterSection) {
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
let mut diagnostic = Diagnostic::new(
NoBlankLineAfterSection {
name: context.section_name().to_string(),
@@ -1657,13 +1658,13 @@ fn common_section(
} else {
// The first blank line is the line containing the closing triple quotes, so we need at
// least two.
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
if checker.enabled(Rule::BlankLineAfterLastSection) {
if checker.enabled(Rule::BlankLineAfterLastSection) {
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
let mut diagnostic = Diagnostic::new(
BlankLineAfterLastSection {
name: context.section_name().to_string(),

View File

@@ -49,5 +49,7 @@ sections.py:558:5: D214 [*] Section is over-indented ("Returns")
563 |- Returns:
563 |+ Returns:
564 564 | """
565 565 |
566 566 |

View File

@@ -61,4 +61,31 @@ sections.py:216:5: D406 [*] Section name should end with a newline ("Raises")
229 229 |
230 230 | """
sections.py:588:5: D406 [*] Section name should end with a newline ("Parameters")
|
587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
| _____^
589 | |
590 | | Parameters:
591 | | ----------
592 | | parameters:
593 | | A list of string parameters
594 | | value:
595 | | Some value
596 | | """
| |_______^ D406
|
= help: Add newline after "Parameters"
Safe fix
587 587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
589 589 |
590 |- Parameters:
590 |+ Parameters
591 591 | ----------
592 592 | parameters:
593 593 | A list of string parameters

View File

@@ -567,5 +567,32 @@ sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns")
563 563 | Returns:
564 |+ -------
564 565 | """
565 566 |
566 567 |
sections.py:600:4: D407 [*] Missing dashed underline after section ("Parameters")
|
599 | def test_lowercase_sub_section_header_different_kind(returns: int):
600 | """Test that lower case subsection header is valid even if it is of a different kind.
| ____^
601 | |
602 | | Parameters
603 | | ------------------
604 | | returns:
605 | | some value
606 | |
607 | | """
| |______^ D407
|
= help: Add dashed line under "Parameters"
Safe fix
600 600 | """Test that lower case subsection header is valid even if it is of a different kind.
601 601 |
602 602 | Parameters
603 |+ ----------
603 604 | ------------------
604 605 | returns:
605 606 | some value

View File

@@ -61,4 +61,39 @@ sections.py:216:5: D409 [*] Section underline should match the length of its nam
227 227 | Raises:
228 228 | My attention.
sections.py:568:5: D409 [*] Section underline should match the length of its name ("Other Parameters")
|
567 | def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213
568 | """Test parameters and attributes sections are capitalized correctly.
| _____^
569 | |
570 | | Parameters
571 | | ----------
572 | | parameters:
573 | | A list of string parameters
574 | | other_parameters:
575 | | A dictionary of string attributes
576 | |
577 | | Other Parameters
578 | | ----------
579 | | other_parameters:
580 | | A dictionary of string attributes
581 | | parameters:
582 | | A list of string parameters
583 | |
584 | | """
| |_______^ D409
|
= help: Adjust underline length to match "Other Parameters"
Safe fix
575 575 | A dictionary of string attributes
576 576 |
577 577 | Other Parameters
578 |- ----------
578 |+ ----------------
579 579 | other_parameters:
580 580 | A dictionary of string attributes
581 581 | parameters:

View File

@@ -21,10 +21,9 @@ D413.py:1:1: D413 [*] Missing blank line after last section ("Returns")
7 7 | Returns:
8 8 | the value
9 |+
10 |+
9 11 | """
10 12 |
11 13 |
9 10 | """
10 11 |
11 12 |
D413.py:13:5: D413 [*] Missing blank line after last section ("Returns")
|

View File

@@ -161,5 +161,33 @@ sections.py:558:5: D413 [*] Missing blank line after last section ("Returns")
563 563 | Returns:
564 |+
564 565 | """
565 566 |
566 567 |
sections.py:588:5: D413 [*] Missing blank line after last section ("Parameters")
|
587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
588 | """Test that lower case subsection header is valid even if it has the same name as section kind.
| _____^
589 | |
590 | | Parameters:
591 | | ----------
592 | | parameters:
593 | | A list of string parameters
594 | | value:
595 | | Some value
596 | | """
| |_______^ D413
|
= help: Add blank line after "Parameters"
Safe fix
593 593 | A list of string parameters
594 594 | value:
595 595 | Some value
596 |+
596 597 | """
597 598 |
598 599 |

View File

@@ -31,7 +31,13 @@ use ruff_text_size::TextSize;
/// ```
///
/// ## References
/// - [mypy](https://mypy.readthedocs.io/en/stable/common_issues.html#spurious-errors-and-locally-silencing-the-checker)
/// Mypy supports a [built-in setting](https://mypy.readthedocs.io/en/stable/error_code_list2.html#check-that-type-ignore-include-an-error-code-ignore-without-code)
/// to enforce that all `type: ignore` annotations include an error code, akin
/// to enabling this rule:
/// ```toml
/// [tool.mypy]
/// enable_error_code = ["ignore-without-code"]
/// ```
#[violation]
pub struct BlanketTypeIgnore;

View File

@@ -84,10 +84,10 @@ pub(crate) fn comparison_with_itself(
{
continue;
}
let [Expr::Name(left_arg)] = left_call.arguments.args.as_slice() else {
let [Expr::Name(left_arg)] = &*left_call.arguments.args else {
continue;
};
let [Expr::Name(right_right)] = right_call.arguments.args.as_slice() else {
let [Expr::Name(right_right)] = &*right_call.arguments.args else {
continue;
};
if left_arg.id != right_right.id {

View File

@@ -59,7 +59,7 @@ pub(crate) fn duplicate_bases(checker: &mut Checker, name: &str, arguments: Opti
let mut seen: FxHashSet<&str> =
FxHashSet::with_capacity_and_hasher(bases.len(), BuildHasherDefault::default());
for base in bases {
for base in bases.iter() {
if let Expr::Name(ast::ExprName { id, .. }) = base {
if !seen.insert(id) {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -45,7 +45,7 @@ impl AlwaysFixableViolation for LiteralMembership {
/// PLR6201
pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompare) {
let [op] = compare.ops.as_slice() else {
let [op] = &*compare.ops else {
return;
};
@@ -53,7 +53,7 @@ pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompa
return;
}
let [right] = compare.comparators.as_slice() else {
let [right] = &*compare.comparators else {
return;
};

View File

@@ -106,7 +106,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel)
range: _,
}) = arg
{
if let [arg] = args.as_slice() {
if let [arg] = &**args {
if arg.as_starred_expr().is_none() {
let new_arg = Expr::Starred(ast::ExprStarred {
value: Box::new(arg.clone()),
@@ -164,8 +164,8 @@ pub(crate) fn nested_min_max(
let flattened_expr = Expr::Call(ast::ExprCall {
func: Box::new(func.clone()),
arguments: Arguments {
args: collect_nested_args(min_max, args, checker.semantic()),
keywords: keywords.to_owned(),
args: collect_nested_args(min_max, args, checker.semantic()).into_boxed_slice(),
keywords: Box::from(keywords),
range: TextRange::default(),
},
range: TextRange::default(),

View File

@@ -96,7 +96,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
};
// Enforced via `is_allowed_value`.
let [right] = comparators.as_slice() else {
let [right] = &**comparators else {
return;
};
@@ -136,14 +136,14 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
checker.generator().expr(&Expr::Compare(ast::ExprCompare {
left: Box::new(value.as_expr().clone()),
ops: match bool_op.op {
BoolOp::Or => vec![CmpOp::In],
BoolOp::And => vec![CmpOp::NotIn],
BoolOp::Or => Box::from([CmpOp::In]),
BoolOp::And => Box::from([CmpOp::NotIn]),
},
comparators: vec![Expr::Tuple(ast::ExprTuple {
comparators: Box::from([Expr::Tuple(ast::ExprTuple {
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
})],
})]),
range: bool_op.range(),
})),
bool_op.range(),
@@ -169,7 +169,7 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
};
// Ignore, e.g., `foo == bar == baz`.
let [op] = ops.as_slice() else {
let [op] = &**ops else {
return false;
};
@@ -181,7 +181,7 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
}
// Ignore self-comparisons, e.g., `foo == foo`.
let [right] = comparators.as_slice() else {
let [right] = &**comparators else {
return false;
};
if ComparableExpr::from(left) == ComparableExpr::from(right) {

View File

@@ -1,10 +1,11 @@
use std::hash::BuildHasherDefault;
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprDict, ExprStringLiteral};
use ruff_python_ast::{Expr, ExprCall, ExprDict, ExprStringLiteral};
use ruff_text_size::Ranged;
use rustc_hash::FxHashSet;
use crate::checkers::ast::Checker;
@@ -37,15 +38,14 @@ impl Violation for RepeatedKeywordArgument {
}
pub(crate) fn repeated_keyword_argument(checker: &mut Checker, call: &ExprCall) {
let ExprCall {
arguments: Arguments { keywords, .. },
..
} = call;
let ExprCall { arguments, .. } = call;
let mut seen =
FxHashSet::with_capacity_and_hasher(keywords.len(), BuildHasherDefault::default());
let mut seen = FxHashSet::with_capacity_and_hasher(
arguments.keywords.len(),
BuildHasherDefault::default(),
);
for keyword in keywords {
for keyword in arguments.keywords.iter() {
if let Some(id) = &keyword.arg {
// Ex) `func(a=1, a=2)`
if !seen.insert(id.as_str()) {

View File

@@ -111,7 +111,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &mut Checker, call: &ast::ExprCal
let mut title: Option<String> = None;
if let Some(dunder) = DunderReplacement::from_method(attr) {
match (call.arguments.args.as_slice(), dunder) {
match (&*call.arguments.args, dunder) {
([], DunderReplacement::Builtin(replacement, message)) => {
if !checker.semantic().is_builtin(replacement) {
return;

View File

@@ -110,7 +110,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
.generator()
.expr(&Expr::StringLiteral(ast::ExprStringLiteral {
value: ast::StringLiteralValue::single(ast::StringLiteral {
value: "locale".to_string(),
value: "locale".to_string().into_boxed_str(),
unicode: false,
range: TextRange::default(),
}),

Some files were not shown because too many files have changed in this diff Show More