Compare commits

...

70 Commits

Author SHA1 Message Date
Zanie
0d0113480c Generate changelog from GitHub releases 2023-07-25 14:19:02 -05:00
Zanie Blue
389fe13c93 Implement visitation of type aliases and parameters (#5927)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #5062 
Requires https://github.com/astral-sh/RustPython-Parser/pull/32

Adds visitation of type alias statements and type parameters in class
and function definitions.

Duplicates tests for `PreorderVisitor` into `Visitor` with new
snapshots. Testing required node implementations for the `TypeParam`
enum, which is a chunk of the diff and the reason we need `Ranged`
implementations in
https://github.com/astral-sh/RustPython-Parser/pull/32.

## Test Plan

<!-- How was it tested? -->

Adds unit tests with snapshots.
2023-07-25 17:11:26 +00:00
Zanie Blue
3000a47fe8 Include file permissions in key for cached files (#5901)
Reimplements https://github.com/astral-sh/ruff/pull/3104
Closes https://github.com/astral-sh/ruff/issues/5726

Note that we will generate the hash for a cache key twice in normal
operation. Once to check for the cached item and again to update the
cache. We could optimize this by generating the hash once in
`diagnostics::lint_file` and passing the `u64` into `get` and `update`.
We'd probably want to wrap it in a `CacheKeyHash` enum for type safety.

## Test plan

Unit tests for Windows and Unix.

Manual test with case from issue

```
❯ touch fake.py
❯ chmod +x fake.py
❯ ./target/debug/ruff --select EXE fake.py
fake.py:1:1: EXE002 The file is executable but no shebang is present
Found 1 error.
❯ chmod -x fake.py
❯ ./target/debug/ruff --select EXE fake.py
```
2023-07-25 17:06:47 +00:00
Charlie Marsh
cbf6085375 Fix example in D413 documentation (#6075)
See #6037.
2023-07-25 12:22:11 -04:00
Charlie Marsh
9171bd4c28 Avoid A003 violations for explicitly overridden methods (#6076)
## Summary

If a method is annotated with `@typing_extensions.override`, we should
avoid flagging A003 on it. This isn't part of the standard library yet,
but it's used to explicitly mark methods as overrides.
2023-07-25 16:21:23 +00:00
Chris Pryer
f5c69c1b34 Update ArgumentsParentheses usage (#6070) 2023-07-25 18:03:48 +02:00
Charlie Marsh
5f63b8bfb8 Ignore some common builtin overrides on standard library subclasses (#6074)
## Summary

If a user subclasses `threading.Event`, e.g. with:

```python
from threading import Event


class CustomEvent(Event):
    def set(self) -> None:
        ...
```

They no control over the method name (`set`). This PR allows
`threading.Event#set` and `logging.Filter#filter` overrides, and avoids
flagging A003 in such cases. Ideally, we'd avoid flagging all overridden
methods, but... that's a lot more difficult, and this is at least
_better_ than what we do now.

Closes https://github.com/astral-sh/ruff/issues/6057.

Closes https://github.com/astral-sh/ruff/issues/5956.
2023-07-25 15:54:34 +00:00
Charlie Marsh
c996b614fe Set default max-complexity to 10 for empty McCabe settings (#6073)
Closes https://github.com/astral-sh/ruff/issues/6058.
2023-07-25 15:38:19 +00:00
Ville Skyttä
670db1db4b pycodestyle.max-doc-length doc updates (#6052) 2023-07-25 15:34:26 +00:00
Charlie Marsh
242cbd966d Perform lint rule analysis after subtree traversal (#6045)
## Summary

This PR modifies the order of operations in our AST checker. Previously,
we ran our analysis rules first, then bound names and traversed over the
subtrees. Now, after a series of refactors, we can invert the order: do
the subtree traversal and model-building _first_, then run rules.

The nice thing about this change is that when we go to analyze, e.g., a
function call node, we'll already have traversed any of the constituent
`Expr::Name` nodes... So if we store the resolution of all names when do
the traversal, we can avoid having to do any expensive work in
`resolve_call_path`.

## Test Plan

Clean run of the snapshot tests, and hopefully the ecosystem checks too!
2023-07-25 09:05:44 -04:00
konsti
e7f228f781 Placement refactor (#6034)
## Summary

This PR is a refactoring of placement.rs. The code got more consistent,
some comments were updated and some dead code was removed or replaced
with debug assertions. It also contains a bugfix for the placement of
end-of-branch comments with nested bodies inside try statements that
occurred when refactoring the nested body loop.

## Test Plan

The existing test cases don't change. I added a couple of cases that i
think should be tested but weren't, and a regression test for the bugfix
2023-07-25 11:49:05 +02:00
Paul Mairo
51d8fc1f30 Update contributing.md with where to run ruff from (#6048)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

As of right now, the instructions don't specify where to run ruff from
after cloning the repository this is to address that. Super trivial
change, but helpful for real newbies I think.
2023-07-24 19:44:55 -04:00
Charlie Marsh
ed72c027a3 Replace NoHashHasher usages with FxHashMap (#6049)
## Summary

I had always assumed that `NoHashHasher` would be faster when using
integer keys, but benchmarking shows otherwise:

```
linter/default-rules/numpy/globals.py
                        time:   [66.544 µs 66.606 µs 66.678 µs]
                        thrpt:  [44.253 MiB/s 44.300 MiB/s 44.342 MiB/s]
                 change:
                        time:   [-0.1843% +0.1087% +0.3718%] (p = 0.46 > 0.05)
                        thrpt:  [-0.3704% -0.1086% +0.1847%]
                        No change in performance detected.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild
linter/default-rules/pydantic/types.py
                        time:   [1.3787 ms 1.3811 ms 1.3837 ms]
                        thrpt:  [18.431 MiB/s 18.466 MiB/s 18.498 MiB/s]
                 change:
                        time:   [-0.4827% -0.1074% +0.1927%] (p = 0.56 > 0.05)
                        thrpt:  [-0.1924% +0.1075% +0.4850%]
                        No change in performance detected.
linter/default-rules/numpy/ctypeslib.py
                        time:   [624.82 µs 625.96 µs 627.17 µs]
                        thrpt:  [26.550 MiB/s 26.601 MiB/s 26.650 MiB/s]
                 change:
                        time:   [-0.7071% -0.4908% -0.2736%] (p = 0.00 < 0.05)
                        thrpt:  [+0.2744% +0.4932% +0.7122%]
                        Change within noise threshold.
linter/default-rules/large/dataset.py
                        time:   [3.1585 ms 3.1634 ms 3.1685 ms]
                        thrpt:  [12.840 MiB/s 12.861 MiB/s 12.880 MiB/s]
                 change:
                        time:   [-1.5338% -1.3463% -1.1476%] (p = 0.00 < 0.05)
                        thrpt:  [+1.1610% +1.3647% +1.5577%]
                        Performance has improved.

linter/all-rules/numpy/globals.py
                        time:   [140.17 µs 140.37 µs 140.58 µs]
                        thrpt:  [20.989 MiB/s 21.020 MiB/s 21.051 MiB/s]
                 change:
                        time:   [-0.1066% +0.3140% +0.7479%] (p = 0.14 > 0.05)
                        thrpt:  [-0.7423% -0.3130% +0.1067%]
                        No change in performance detected.
Found 3 outliers among 100 measurements (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe
linter/all-rules/pydantic/types.py
                        time:   [2.7030 ms 2.7069 ms 2.7112 ms]
                        thrpt:  [9.4064 MiB/s 9.4216 MiB/s 9.4351 MiB/s]
                 change:
                        time:   [-0.6721% -0.4874% -0.2974%] (p = 0.00 < 0.05)
                        thrpt:  [+0.2982% +0.4898% +0.6766%]
                        Change within noise threshold.
Found 14 outliers among 100 measurements (14.00%)
  12 (12.00%) high mild
  2 (2.00%) high severe
linter/all-rules/numpy/ctypeslib.py
                        time:   [1.4709 ms 1.4727 ms 1.4749 ms]
                        thrpt:  [11.290 MiB/s 11.306 MiB/s 11.320 MiB/s]
                 change:
                        time:   [-1.1617% -0.9766% -0.8094%] (p = 0.00 < 0.05)
                        thrpt:  [+0.8160% +0.9862% +1.1754%]
                        Change within noise threshold.
Found 12 outliers among 100 measurements (12.00%)
  9 (9.00%) high mild
  3 (3.00%) high severe
linter/all-rules/large/dataset.py
                        time:   [5.8086 ms 5.8163 ms 5.8240 ms]
                        thrpt:  [6.9854 MiB/s 6.9946 MiB/s 7.0038 MiB/s]
                 change:
                        time:   [-1.5651% -1.3536% -1.1584%] (p = 0.00 < 0.05)
                        thrpt:  [+1.1720% +1.3721% +1.5900%]
                        Performance has improved.
```

My guess is that `NoHashHasher` underperforms because the keys are not
randomly distributed...

Anyway, it's a ~1% (significant) performance gain on some of the above,
plus we get to remove a dependency.
2023-07-24 23:41:57 +00:00
Charlie Marsh
b7e7346081 Remove empty newline in deferred_for_loops (#6046)
Trivial change but none of the others have this empty newline.
2023-07-24 21:59:32 +00:00
Charlie Marsh
d35b5248ea Tweak lambda rule to use annotations rather than shadowing (#6044)
## Summary

This PR ensures that we can retain the current behavior even after we
reorder the visitor a bit, by looking for annotated lambdas rather than
"is the name bound to anything?", since if we visit the name before we
run this rule, it'll _always_ be bound. (This check is already a bit
flawed -- in truth, we should probably run this rule deferred so that we
can reliably detect shadowing.)
2023-07-24 21:39:02 +00:00
Charlie Marsh
c535e10fff Move comprehension rules into shared analyze method (#6042) 2023-07-24 21:18:45 +00:00
Charlie Marsh
c3ecdb8783 Fix Arg typo (#6041) 2023-07-24 21:16:28 +00:00
Charlie Marsh
242df67cbf Move lint rules out of checkers/ast/mod.rs (#5957)
## Summary

This PR attempts to draw some basic separation between the `Checker`'s
traversal responsibilities (traversing the AST, building the semantic
model) and its calling-out-to-lint-rule responsibilities. It doesn't try
to introduce any sophisticated API. Instead, it just moves all of the
lint rule calls out of `checkers/ast/mod.rs` and into methods in a new
`analyze` module. (There are four remaining lint rules in `Checker`, but
I'll remove those in future PRs.)

I'm not trying to "solve" our lint rule API here. Instead, I'm trying to
make two improvements:

1. `checkers/ast/mod.rs` has just gotten way too large, and people work
in it all the time. Prior to this PR, it was 5.5k lines, which led to
significant lags in my editor and made it really hard to reason about
the parts that are _actually_ important. (I like big files, but this one
crossed the line for me.) Now, it's < 2,000 lines, and the code is much
more focused.
2. I want to avoid accidentally adding lint rules in the "wrong" parts
of the traversal. By confining lint rule invocations to these "analyze"
calls, we'll avoid (e.g.) putting them in the binding phase.
2023-07-24 19:20:10 +00:00
Charlie Marsh
776d598738 Move flake8-executable rules out of physical lines checker (#6039)
## Summary

These only need the token stream, and we always prefer token-based to
physical line-based rules.

There are a few other changes snuck in here:

- Renaming the rule files to match the diagnostic names (likely an
error).
- The "leading whitespace before shebang" rule now works regardless of
where the comment occurs (i.e., if the shebang is on the second line,
and the first line is blank, we flag and remove that leading
whitespace).
2023-07-24 14:38:05 -04:00
konsti
7f3797185c Fix formatter with-statement after-as own line comment instability (#6033)
**Summary** Fix an instability in with statement formatter when there is
an own line comment as the `as`
```python
with (
    a as
    # bad comment
    b):
```

**Test Plan** Added the comment to the test cases.
2023-07-24 18:12:07 +00:00
konsti
a9f535997d Document formatter progress scripts (#6035)
## Summary

Add documentation to the formatter progress scripts

## Test Plan

n/a
2023-07-24 19:42:20 +02:00
Micha Reiser
fdb3c8852f Prefer breaking the implicit string concatenation over breaking before % (#5947) 2023-07-24 18:30:42 +02:00
Charlie Marsh
42d969f19f Add additional test cases for F823 (#6036)
Making some behavior explicit / codified. See:
https://github.com/astral-sh/ruff/issues/6029.
2023-07-24 15:49:48 +00:00
Charlie Marsh
62ffc773de Avoid treating Literal members as expressions with __future__ (#6032)
Closes https://github.com/astral-sh/ruff/issues/6030.
2023-07-24 15:09:37 +00:00
Charlie Marsh
6feb3fcc1b Ignore end-of-line comments when dirtying if-with-same-arms branches (#6031)
## Summary

Closes https://github.com/astral-sh/ruff/issues/6025 (which contains a
more thorough description of the issue). Previously, the `# noqa` here
was being marked as unused, but removing it raised `SIM114`:

```python
def foo():
    a = True
    b = False
    if a > b:  # noqa: SIM114
        return 3
    elif a == b:
        return 3
```
2023-07-24 10:59:58 -04:00
Chris Pryer
8eadacda33 Update TupleParentheses usage (#5810) 2023-07-24 14:44:36 +00:00
konsti
8a7dcb794b Add formatter progress tracking to CI (#5919)
**Summary** Add a formatter progress testing script to CI. This script
will 1) print the black compability on each run 2) catch regressions wrt
to formatter stability, emitting invalid syntax and other kinds of bugs
(e.g. #5917) before they land on main 3) have an additional layer of
real world tests when implementing new nodes or other new formatter
code.

This is currently a bash script, i'm not sure if we want to keep it that
way, or switch to e.g. the regular ecosystem scripts. The output
separation of `format_dev` could also use some polishing. We should also
consider pinning commits so we don't get spurious regression when they
change their code.

**Test Plan** The script extends CI.
2023-07-24 09:12:42 +00:00
Luc Khai Hai
dfa81b6fe0 Format numeric constants (#5972)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-07-24 07:04:40 +00:00
Charlie Marsh
33196f1859 Fix logging rules with whitespace around dot (#6022)
## Summary

Attempting to fix, e.g., `logging . warn("Hello World!")` was causing a
syntax error.
2023-07-24 05:14:48 +00:00
Charlie Marsh
0d94337b96 Avoid allocations in SimpleCallArgs (#6021)
## Summary

My intuition is that it's faster to do these checks as-needed rather
than allocation new hash maps and vectors for the arguments. (We
typically only query once anyway.)
2023-07-24 04:55:37 +00:00
Charlie Marsh
f9726af4ef Allow specification of logging.Logger re-exports via logger-objects (#5750)
## Summary

This PR adds a `logger-objects` setting that allows users to mark
specific symbols a `logging.Logger` objects. Currently, if a `logger` is
imported, we only flagged it as a `logging.Logger` if it comes exactly
from the `logging` module or is `flask.current_app.logger`.

This PR allows users to mark specific loggers, like
`logging_setup.logger`, to ensure that they're covered by the
`flake8-logging-format` rules and others.

For example, if you have a module `logging_setup.py` with the following
contents:

```python
import logging

logger = logging.getLogger(__name__)
```

Adding `"logging_setup.logger"` to `logger-objects` will ensure that
`logging_setup.logger` is treated as a `logging.Logger` object when
imported from other modules (e.g., `from logging_setup import logger`).

Closes https://github.com/astral-sh/ruff/issues/5694.
2023-07-24 00:38:20 -04:00
Tom Kuson
727153cf45 [pylint] Impement self-assigning-variable (W0127) (#6015)
## Summary

Implements Pylint rule [`self-assigning-variable`
(`W0127`)](https://pylint.pycqa.org/en/latest/user_guide/messages/warning/self-assigning-variable.html)
as `self-assigning-variable` (`PLW0127`). Includes documentation.
Related to #970.

## Test Plan

`cargo test`
2023-07-24 02:27:09 +00:00
Charlie Marsh
574c0e0105 Use match instead of phf for confusable lookup (#5953)
I don't know whether we want to make this change but here's some data...

Binary size:

- `main`: 30,384
- `charlie/match-phf`: 30,416

llvm-lines:

- `main`: 1,784,148
- `charlie/match-phf`: 1,789,877

llvm-lines and binary size are both unchanged (or, by < 5) when moving
from `u8` to `u32` return types, and even when moving to `char` keys and
values. I didn't expect this, but I'm not very knowledgable on this
topic.

Performance:

```
Confusables/match/src   time:   [4.9102 µs 4.9352 µs 4.9777 µs]
                        change: [+1.7469% +2.2421% +2.8710%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 12 outliers among 100 measurements (12.00%)
  2 (2.00%) low mild
  4 (4.00%) high mild
  6 (6.00%) high severe
Confusables/match-with-skip/src
                        time:   [2.0676 µs 2.0945 µs 2.1317 µs]
                        change: [+0.9384% +1.6000% +2.3920%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 8 outliers among 100 measurements (8.00%)
  3 (3.00%) high mild
  5 (5.00%) high severe
Confusables/phf/src     time:   [31.087 µs 31.188 µs 31.305 µs]
                        change: [+1.9262% +2.2188% +2.5496%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 15 outliers among 100 measurements (15.00%)
  3 (3.00%) low mild
  6 (6.00%) high mild
  6 (6.00%) high severe
Confusables/phf-with-skip/src
                        time:   [2.0470 µs 2.0486 µs 2.0502 µs]
                        change: [-0.3093% -0.1446% +0.0106%] (p = 0.08 > 0.05)
                        No change in performance detected.
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) high mild
  2 (2.00%) high severe
```

The `-with-skip` variants add our optimization which first checks
whether the character is ASCII. So `match` is way, way faster than PHF,
but it tends not to matter since almost all source code is ASCII anyway.
2023-07-24 02:23:36 +00:00
Dhruv Manilawala
700c816fd5 Make TRY201 always autofixable (#6008)
## Summary

Make `TRY201` always autofiable.

## Test Plan

1. `cargo test`
2. `cargo insta review`

ref:
https://github.com/astral-sh/ruff/issues/4333#issuecomment-1646359788
2023-07-24 02:23:15 +00:00
Tom Kuson
3b56f6d616 [pylint] Implement subprocess-popen-preexec-fn (W1509) (#5978)
## Summary

Implements Pylint rule [`subprocess-popen-preexec-fn`
(`W1509`)](https://pylint.pycqa.org/en/latest/user_guide/messages/warning/subprocess-popen-preexec-fn.html)
as `subprocess-popen-preexec-fn` (`PLW1509`). Includes documentation.
Related to #970.

## Test Plan

`cargo test`
2023-07-24 02:06:19 +00:00
Harutaka Kawamura
110fa804ff Add PT016 documentation (#6005) 2023-07-23 21:52:48 -04:00
Harutaka Kawamura
2b9c22de0f Add a unit test for python-file-like directory exclusion (#5997) 2023-07-24 01:50:39 +00:00
Harutaka Kawamura
51ebff7e41 Add PT010 doc (#6010) 2023-07-24 01:43:18 +00:00
Dhruv Manilawala
742f615792 Add support for int, float, bool in UP018 (#6013)
## Summary

This pull request add supports for `int`, `float` and `bool` types in
`UP018`
rule to convert empty call to the default value of the type or remove
the call
if a value of the same type is provided as an argument.

## Test Plan

Added tests for `int`, `float` and `bool` types.

Partially resolves #5988
2023-07-23 21:39:43 -04:00
Harutaka Kawamura
95e6258d5d Add PT020 doc (#6011) 2023-07-23 21:37:03 -04:00
Dhruv Manilawala
5dbb4dd823 Update docs for ANN401 (#6009)
Part of #5803
2023-07-23 16:15:04 +00:00
konsti
46f8961292 Formatter: Add EmptyWithDanglingComments helper (#5951)
**Summary** Add a `EmptyWithDanglingComments` format helper that formats
comments inside empty parentheses, brackets or curly braces. Previously,
this was implemented separately, and partially incorrectly, for each use
case.

Empty `()`, `[]` and `{}` are special because there can be dangling
comments, and they can be in
two positions:
```python
x = [  # end-of-line
    # own line
]
```
These comments are dangling because they can't be assigned to any
element inside as they would
in all other cases.

**Test Plan** Added a regression test.

145 (from previously 149) instances of unstable formatting remaining.

```
$ cargo run --bin ruff_dev --release -- format-dev --stability-check --error-file formatter-ecosystem-errors.txt --multi-project target/checkouts > formatter-ecosystem-progress.txt
$ rg "Unstable formatting" target/formatter-ecosystem-errors.txt | wc -l
145
```
2023-07-23 14:32:16 +02:00
Simon Brugman
f886b58c92 [flake8-use-pathlib] Implement os-sep-split (PTH206) (#5936)
Implements
https://github.com/astral-sh/ruff/issues/5905#issuecomment-1644822548

---------

Co-authored-by: konsti <konstin@mailbox.org>
2023-07-23 12:22:26 +02:00
Charlie Marsh
057faabcdd Use Flags::intersects rather than Flags::contains (#6007)
## Summary

This is equivalent for a single flag, but I think it's more likely to be
correct when the bitflags are modified -- the primary reason being that
we sometimes define flags as the union of other flags, e.g.:

```rust
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();
```

In this case, `flags.contains(Flag::ANNOTATION)` requires that _both_
flags in the union are set, whereas `flags.intersects(Flag::ANNOTATION)`
requires that _at least one_ flag is set.
2023-07-23 02:59:31 +00:00
Charlie Marsh
0bb175f7f6 Store flags rather than ExecutionContext on references (#6006) 2023-07-23 02:54:39 +00:00
Charlie Marsh
4b2ec7d562 Move runtime execution context into add_reference calls (#6003) 2023-07-23 02:37:51 +00:00
Charlie Marsh
4aac801277 Fix context-to-model references in SemanticModel documentation (#6004) 2023-07-23 02:32:23 +00:00
Charlie Marsh
45a24912a6 Remove extra error! call (#6002) 2023-07-23 02:29:06 +00:00
Simon Brugman
3914fcb7ca Extend SIM118 with not in (#5995)
Closes https://github.com/astral-sh/ruff/issues/5989

Tracking issue https://github.com/astral-sh/ruff/issues/1348
2023-07-23 01:46:21 +00:00
Charlie Marsh
6d58b773b1 Use simple text matching for type: ignore detection (#5999)
Closes #5980.
2023-07-23 01:45:28 +00:00
Tom Kuson
e7f5121922 Extends B002 to detect unary prefix decrement operators (#5998)
## Summary

Extends `B002` to detect unary decrement prefix operators.

Closes #5992.

## Test Plan

`cargo test`
2023-07-23 01:40:49 +00:00
Charlie Marsh
1776cbd2e2 Move blanket noqa and blanket type: ignore rules into token-based checker (#5996)
Closes https://github.com/astral-sh/ruff/issues/5981.
2023-07-22 21:22:48 -04:00
Charlie Marsh
71f1643eda Use memchr for invalid-escape-sequence (#5994) 2023-07-22 20:57:36 -04:00
Tom Kuson
74dc137b30 Use find_keyword helper function in more places (#5993)
## Summary

Use the `find_keyword` helper function instead of reimplementing it.

Follows on from #5983 by doing a different search.

## Test Plan

`cargo test`
2023-07-22 20:27:24 -04:00
Harutaka Kawamura
97e31cad2f Fix F507 false positive (#5986)
## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

F507 should not be raised when the right-hand side value is a non-tuple
object.

```python
'%s' % (1, 2, 3)  # throws
'%s' % [1, 2, 3]  # doesn't throw
'%s' % {1, 2, 3}  # doesn't throw
```
2023-07-22 18:42:44 +00:00
Simon Brugman
ed7d2b8a3d Do not raise SIM105 for non-exceptions (#5985)
Closes https://github.com/astral-sh/ruff/issues/5977

Added a test case from `refurb`
2023-07-22 18:36:46 +00:00
Tom Kuson
c7e4c58181 Use find_keyword helper function (#5983)
## Summary

Use `find_keyword` helper function instead of reimplementing it.

## Test Plan

`cargo test`
2023-07-22 14:09:30 -04:00
Charlie Marsh
6ff566f2c1 Flag [ as an invalid noqa suffix (#5982)
Closes https://github.com/astral-sh/ruff/issues/5960.
2023-07-22 10:16:28 -04:00
Charlie Marsh
32773e8309 Move locator, stylist, and friends better getters (#5968)
## Summary

Rather than exposing these as public fields, use getters, similar to
`semantic()`.
2023-07-22 09:37:24 -04:00
Harutaka Kawamura
050f5953f8 Avoid raising UP032 if format call arguments contain multiline expressions (#5971)
## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fix a regression introduced by
https://github.com/astral-sh/ruff/pull/5638. A multiline expression
can't be safely inserted into a format field.

### Example

```
> cat a.py
"{}".format(
    [
        1,
        2,
        3,
    ]
)

> cargo run -p ruff_cli -- check a.py --no-cache --select UP032 --fix
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/ruff check a.py --no-cache --select UP032 --fix`
error: Autofix introduced a syntax error in `a.py` with rule codes UP032: EOL while scanning string literal at byte offset 5
---
f"{[
        1,
        2,
        3,
    ]}"

---
a.py:1:1: UP032 Use f-string instead of `format` call
Found 1 error.
```


## Test Plan

New test cases
2023-07-22 09:37:08 -04:00
Alex Waygood
aba340a177 Fix typo in PYI056 docs (#5973)
The current "use instead" code would correctly be rejected by any type
checker worth its salt ;)
2023-07-22 09:10:38 -04:00
Victor Hugo Gomes
33657d3a1c [flake8-pyi] Implement PYI056 (#5959)
## Summary

Checks that `append`, `extend` and `remove` methods are not called on
`__all__`. See [original
implementation](2a86db8271/pyi.py (L1133-L1138)).

```
$ flake8 --select Y026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi

crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:3:1: Y056 Calling ".append()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:4:1: Y056 Calling ".extend()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:5:1: Y056 Calling ".remove()" on "__all__" may not be supported by all type checkers (use += instead)
```

```
$ ./target/debug/ruff --select PYI026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi --no-cache

crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:3:1: PYI056 Calling ".append()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:4:1: PYI056 Calling ".extend()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:5:1: PYI056 Calling ".remove()" on "__all__" may not be supported by all type checkers (use += instead)
Found 3 errors.
```

ref #848

## Test Plan

Snapshots and manual runs of flake8.
2023-07-22 04:25:54 +00:00
Charlie Marsh
45318d08b7 Always compute runtime annotations for flake8-type-checking rules (#5967)
## Summary

These are skipped as an optimization, but it feels kind of unnecessary
and makes the code a bit more confusing than is worthwhile.
(non-`strict` is also by far the more popular setting, and the default.)
2023-07-21 23:53:33 -04:00
Charlie Marsh
86b6a3e1ad Remove nested f-string flag (#5966)
## Summary

Not worth taking up a slot in the semantic model flags.
2023-07-21 22:51:37 -04:00
Charlie Marsh
f5a2fb5b5d Bump version to 0.0.280 (#5965) 2023-07-21 22:36:13 -04:00
Charlie Marsh
94a004ee9c Avoid collapsing elif and else branches during import sorting (#5964)
## Summary

I ran into this in the wild. It looks like Ruff will collapse the `else`
and `elif` branches here (i.e., it doesn't recognize that they're too
independent import blocks):

```python
if "sdist" in cmds:
    _sdist = cmds["sdist"]
elif "setuptools" in sys.modules:
    from setuptools.command.sdist import sdist as _sdist
else:
    from setuptools.command.sdist import sdist as _sdist
    from distutils.command.sdist import sdist as _sdist
```

Likely fallout from the `elif_else_branches` refactor.
2023-07-22 02:18:02 +00:00
Tom Kuson
aaf7f362a1 Create snake_case file if linter is Pylint (#5948)
## Summary

The `add_rule.py` script would create a test case that pointed to a file
that didn't exist when the linter is set to `"pylint"`. This PR fixes
that.

## Test Plan

`python scripts/add_rule.py --name DoTheThing --prefix PL --code C0999
--linter pylint`
2023-07-21 22:13:43 -04:00
Charlie Marsh
2dcd9e2e9c Remove unnecessary check_deferred_assignments (#5963)
## Summary

These rules can just be included in the `check_deferred_scopes`.
2023-07-22 02:08:44 +00:00
Charlie Marsh
40e9884353 Move nonlocal-without-binding out of binding step (#5962) 2023-07-22 01:39:27 +00:00
Tom Kuson
9bbb0a5151 Fix typo in documentation (#5961)
## Summary

Close unclosed inline code block that was causing the text not to render
properly.

## Test Plan

`mkdocs serve`
2023-07-22 01:23:30 +00:00
337 changed files with 16293 additions and 8248 deletions

View File

@@ -319,8 +319,8 @@ jobs:
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.generated.yml
check-formatter-stability:
name: "Check formatter stability"
check-formatter-ecosystem:
name: "Formatter ecosystem and progress checks"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true'
@@ -330,7 +330,12 @@ jobs:
run: rustup show
- name: "Cache rust"
uses: Swatinem/rust-cache@v2
- name: "Formatter progress"
run: scripts/formatter_progress.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_report.txt | sort > $GITHUB_STEP_SUMMARY
# CPython is not black formatted, so we run only the stability check
- name: "Clone CPython 3.10"
run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
- name: "Check stability"
- name: "Check CPython stability"
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython

4080
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
### Development
After cloning the repository, run Ruff locally with:
After cloning the repository, run Ruff locally from the repository root with:
```shell
cargo run -p ruff_cli -- check /path/to/file.py --no-cache
@@ -156,10 +156,13 @@ At a high level, the steps involved in adding a new lint rule are as follows:
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
an `ast::StmtAssert` node).
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
1. Define the logic for invoking the diagnostic in `crates/ruff/src/checkers/ast/analyze` (for
AST-based rules), `crates/ruff/src/checkers/tokens.rs` (for token-based rules),
`crates/ruff/src/checkers/physical_lines.rs` (for text-based rules),
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based rules), etc. For AST-based rules,
you'll likely want to modify `analyze/statement.rs` (if your rule is based on analyzing
statements, like imports) or `analyze/expression.rs` (if your rule is based on analyzing
expressions, like function calls).
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`).

30
Cargo.lock generated
View File

@@ -734,7 +734,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.279"
version = "0.0.280"
dependencies = [
"anyhow",
"clap",
@@ -1295,12 +1295,6 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1524,7 +1518,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared",
]
@@ -1548,19 +1541,6 @@ dependencies = [
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
@@ -1888,7 +1868,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.279"
version = "0.0.280"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1909,14 +1889,12 @@ dependencies = [
"log",
"memchr",
"natord",
"nohash-hasher",
"num-bigint",
"num-traits",
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs",
"phf",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -1988,7 +1966,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.279"
version = "0.0.280"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2017,6 +1995,7 @@ dependencies = [
"ruff",
"ruff_cache",
"ruff_diagnostics",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_stdlib",
@@ -2179,7 +2158,6 @@ version = "0.0.0"
dependencies = [
"bitflags 2.3.3",
"is-macro",
"nohash-hasher",
"num-traits",
"ruff_index",
"ruff_python_ast",

View File

@@ -26,7 +26,6 @@ is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
memchr = "2.5.0"
nohash-hasher = { version = "0.2.0" }
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.279
rev: v0.0.280
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.279"
version = "0.0.280"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.279"
version = "0.0.280"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -45,7 +45,6 @@ libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
nohash-hasher = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
once_cell = { workspace = true }
@@ -55,7 +54,6 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
phf = { version = "0.11", features = ["macros"] }
pyproject-toml = { version = "0.6.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }

View File

@@ -1,6 +1,6 @@
"""
Should emit:
B002 - on lines 15 and 20
B002 - on lines 18, 19, and 24
"""
@@ -8,13 +8,17 @@ def this_is_all_fine(n):
x = n + 1
y = 1 + n
z = +x + y
return +z
a = n - 1
b = 1 - n
c = -a - b
return +z, -c
def this_is_buggy(n):
x = ++n
return x
y = --n
return x, y
def this_is_buggy_too(n):
return ++n
return ++n, --n

View File

@@ -17,3 +17,37 @@ from typing import TypedDict
class MyClass(TypedDict):
id: int
from threading import Event
class CustomEvent(Event):
def set(self) -> None:
...
def str(self) -> None:
...
from logging import Filter, LogRecord
class CustomFilter(Filter):
def filter(self, record: LogRecord) -> bool:
...
def str(self) -> None:
...
from typing_extensions import override
class MyClass:
@override
def str(self):
pass
def int(self):
pass

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env python

View File

@@ -1,5 +1,10 @@
import logging
from distutils import log
from logging_setup import logger
logging.warn("Hello World!")
log.warn("Hello world!") # This shouldn't be considered as a logger candidate
logger.warn("Hello world!")
logging . warn("Hello World!")

View File

@@ -0,0 +1,12 @@
__all__ = ["A", "B", "C"]
# Errors
__all__.append("D")
__all__.extend(["E", "Foo"])
__all__.remove("A")
# OK
__all__ += ["D"]
foo = ["Hello"]
foo.append("World")
foo.bar.append("World")

View File

@@ -0,0 +1,12 @@
__all__ = ["A", "B", "C"]
# Errors
__all__.append("D")
__all__.extend(["E", "Foo"])
__all__.remove("A")
# OK
__all__ += ["D"]
foo = ["Hello"]
foo.append("World")
foo.bar.append("World")

View File

@@ -14,6 +14,12 @@ try:
except (ValueError, OSError):
pass
# SIM105
try:
foo()
except (ValueError, OSError) as e:
pass
# SIM105
try:
foo()
@@ -94,3 +100,13 @@ def with_comment():
foo()
except (ValueError, OSError):
pass # Trailing comment.
try:
print()
except ("not", "an", "exception"):
pass
try:
print()
except "not an exception":
pass

View File

@@ -101,3 +101,16 @@ if a:
x = 1
elif c:
x = 1
def foo():
a = True
b = False
if a > b: # end-of-line
return 3
elif a == b:
return 3
elif a < b: # end-of-line
return 4
elif b is None:
return 4

View File

@@ -1,11 +1,19 @@
key in obj.keys() # SIM118
key not in obj.keys() # SIM118
foo["bar"] in obj.keys() # SIM118
foo["bar"] not in obj.keys() # SIM118
foo['bar'] in obj.keys() # SIM118
foo['bar'] not in obj.keys() # SIM118
foo() in obj.keys() # SIM118
foo() not in obj.keys() # SIM118
for key in obj.keys(): # SIM118
pass

View File

@@ -0,0 +1,20 @@
import os
from os import sep
file_name = "foo/bar"
# PTH206
"foo/bar/".split(os.sep)
"foo/bar/".split(sep=os.sep)
"foo/bar/".split(os.sep)[-1]
"foo/bar/".split(os.sep)[-2]
"foo/bar/".split(os.sep)[-2:]
"fizz/buzz".split(sep)
"fizz/buzz".split(sep)[-1]
os.path.splitext("path/to/hello_world.py")[0].split(os.sep)[-1]
file_name.split(os.sep)
(os.path.abspath(file_name)).split(os.sep)
# OK
"foo/bar/".split("/")

View File

@@ -0,0 +1,7 @@
if "sdist" in cmds:
_sdist = cmds["sdist"]
elif "setuptools" in sys.modules:
from setuptools.command.sdist import sdist as _sdist
else:
from setuptools.command.sdist import sdist as _sdist
from distutils.command.sdist import sdist as _sdist

View File

@@ -0,0 +1,7 @@
match 1:
case 1:
import sys
import os
case 2:
import collections
import abc

View File

@@ -41,3 +41,5 @@ regex = '\w' # noqa
regex = '''
\w
''' # noqa
regex = '\\\_'

View File

@@ -23,3 +23,5 @@ a = []
'%s %s' % (*a,)
k = {}
'%(k)s' % {**k}
'%s' % [1, 2, 3]
'%s' % {1, 2, 3}

View File

@@ -0,0 +1,11 @@
"""Test case: `Literal` with `__future__` annotations."""
from __future__ import annotations
from typing import Literal, Final
from typing_extensions import assert_type
CONSTANT: Final = "ns"
assert_type(CONSTANT, Literal["ns"])

View File

@@ -39,3 +39,27 @@ class Class:
def f(self):
print(my_var)
my_var = 1
import sys
def main():
print(sys.argv)
try:
3 / 0
except ZeroDivisionError:
import sys
sys.exit(1)
import sys
def main():
print(sys.argv)
for sys in range(5):
pass

View File

@@ -1,6 +1,7 @@
x = 1 # type: ignore
x = 1 # type:ignore
x = 1 # type: ignore[attr-defined] # type: ignore
x = 1 # type: ignoreme # type: ignore
x = 1
x = 1 # type ignore

View File

@@ -0,0 +1,41 @@
foo = 1
bar = 2
baz = 3
# Errors.
foo = foo
bar = bar
foo, bar = foo, bar
bar, foo = bar, foo
(foo, bar) = (foo, bar)
(bar, foo) = (bar, foo)
foo, (bar, baz) = foo, (bar, baz)
bar, (foo, baz) = bar, (foo, baz)
(foo, bar), baz = (foo, bar), baz
(foo, (bar, baz)) = (foo, (bar, baz))
foo, bar = foo, 1
bar, foo = bar, 1
(foo, bar) = (foo, 1)
(bar, foo) = (bar, 1)
foo, (bar, baz) = foo, (bar, 1)
bar, (foo, baz) = bar, (foo, 1)
(foo, bar), baz = (foo, bar), 1
(foo, (bar, baz)) = (foo, (bar, 1))
foo: int = foo
bar: int = bar
# Non-errors.
foo = bar
bar = foo
foo, bar = bar, foo
foo, bar = bar, foo
(foo, bar) = (bar, foo)
foo, bar = bar, 1
bar, foo = foo, 1
foo: int = bar
bar: int = 1
class Foo:
foo = foo
bar = bar

View File

@@ -0,0 +1,18 @@
import subprocess
def foo():
pass
# Errors.
subprocess.Popen(preexec_fn=foo)
subprocess.Popen(["ls"], preexec_fn=foo)
subprocess.Popen(preexec_fn=lambda: print("Hello, world!"))
subprocess.Popen(["ls"], preexec_fn=lambda: print("Hello, world!"))
# Non-errors.
subprocess.Popen()
subprocess.Popen(["ls"])
subprocess.Popen(preexec_fn=None) # None is the default.
subprocess.Popen(["ls"], preexec_fn=None) # None is the default.

View File

@@ -15,7 +15,22 @@ bytes("foo", **a)
bytes(b"foo"
b"bar")
bytes("foo")
bytes(1)
f"{f'{str()}'}"
int(1.0)
int("1")
int(b"11")
int(10, base=2)
int("10", base=2)
int("10", 2)
float("1.0")
float(b"1.0")
bool(1)
bool(0)
bool("foo")
bool("")
bool(b"")
bool(1.0)
# These become string or byte literals
str()
@@ -27,3 +42,10 @@ bytes(b"foo")
bytes(b"""
foo""")
f"{str()}"
int()
int(1)
float()
float(1.0)
bool()
bool(True)
bool(False)

View File

@@ -124,6 +124,22 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
111111
)
"{}".format(
[
1,
2,
3,
]
)
"{a}".format(
a=[
1,
2,
3,
]
)
async def c():
return "{}".format(await 3)

View File

@@ -1,9 +1,8 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use nohash_hasher::IntSet;
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
use ruff_python_ast::source_code::Locator;
@@ -47,7 +46,7 @@ fn apply_fixes<'a>(
let mut output = String::with_capacity(locator.len());
let mut last_pos: Option<TextSize> = None;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut isolated: IntSet<u32> = IntSet::default();
let mut isolated: FxHashSet<u32> = FxHashSet::default();
let mut fixed = FxHashMap::default();
let mut source_map = SourceMap::default();

View File

@@ -0,0 +1,27 @@
use rustpython_parser::ast::{Arg, Ranged};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
/// Run lint rules over an [`Arg`] syntax node.
pub(crate) fn argument(arg: &Arg, checker: &mut Checker) {
if checker.enabled(Rule::AmbiguousVariableName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_variable_name(&arg.arg, arg.range())
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidArgumentName) {
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
&arg.arg,
arg,
&checker.settings.pep8_naming.ignore_names,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BuiltinArgumentShadowing) {
flake8_builtins::rules::builtin_argument_shadowing(checker, arg);
}
}

View File

@@ -0,0 +1,26 @@
use rustpython_parser::ast::Arguments;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Arguments`] syntax node.
pub(crate) fn arguments(arguments: &Arguments, checker: &mut Checker) {
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, arguments);
}
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, arguments);
}
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, arguments);
}
if checker.is_stub {
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, arguments);
}
if checker.enabled(Rule::ArgumentDefaultInStub) {
flake8_pyi::rules::argument_simple_defaults(checker, arguments);
}
}
}

View File

@@ -0,0 +1,68 @@
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
use ruff_diagnostics::{Diagnostic, Fix};
/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::InvalidAllFormat,
Rule::InvalidAllObject,
Rule::UnaliasedCollectionsAbcSetImport,
Rule::UnconventionalImportAlias,
Rule::UnusedVariable,
]) {
return;
}
for binding in checker.semantic.bindings.iter() {
if checker.enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception() && !binding.is_used() {
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),
},
binding.range,
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {
pyflakes::fixes::remove_exception_handler_assignment(
binding,
checker.locator,
)
.map(Fix::automatic)
});
}
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidAllFormat) {
if let Some(diagnostic) = pylint::rules::invalid_all_format(binding) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidAllObject) {
if let Some(diagnostic) = pylint::rules::invalid_all_object(binding) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UnconventionalImportAlias) {
if let Some(diagnostic) = flake8_import_conventions::rules::unconventional_import_alias(
checker,
binding,
&checker.settings.flake8_import_conventions.aliases,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
if let Some(diagnostic) =
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding)
{
checker.diagnostics.push(diagnostic);
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
use rustpython_parser::ast::Comprehension;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::flake8_simplify;
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(
checker,
&comprehension.target,
&comprehension.iter,
);
}
}

View File

@@ -0,0 +1,32 @@
use rustpython_parser::ast::{self, Stmt};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, perflint};
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
while !checker.deferred.for_loops.is_empty() {
let for_loops = std::mem::take(&mut checker.deferred.for_loops);
for snapshot in for_loops {
checker.semantic.restore(snapshot);
if let Stmt::For(ast::StmtFor {
target, iter, body, ..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
} else {
unreachable!("Expected Expr::For | Expr::AsyncFor");
}
}
}
}

View File

@@ -0,0 +1,287 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
pub(crate) fn deferred_scopes(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::GlobalVariableNotAssigned,
Rule::ImportShadowedByLoopVar,
Rule::RedefinedWhileUnused,
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
Rule::UndefinedLocal,
Rule::UnusedAnnotation,
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
Rule::UnusedImport,
Rule::UnusedLambdaArgument,
Rule::UnusedMethodArgument,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
]) {
return;
}
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_imports = !checker.is_stub
&& checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
]);
let runtime_imports: Vec<Vec<&Binding>> = if enforce_typing_imports {
checker
.semantic
.scopes
.iter()
.map(|scope| {
scope
.binding_ids()
.map(|binding_id| checker.semantic.binding(binding_id))
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(
binding,
&checker.semantic,
)
})
.collect()
})
.collect::<Vec<_>>()
} else {
vec![]
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope_id in checker.deferred.scopes.iter().rev().copied() {
let scope = &checker.semantic.scopes[scope_id];
if checker.enabled(Rule::UndefinedLocal) {
pyflakes::rules::undefined_local(checker, scope_id, scope, &mut diagnostics);
}
if checker.enabled(Rule::GlobalVariableNotAssigned) {
for (name, binding_id) in scope.bindings() {
let binding = checker.semantic.binding(binding_id);
if binding.kind.is_global() {
diagnostics.push(Diagnostic::new(
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range,
));
}
}
}
if checker.enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &checker.semantic.bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
}
}
if checker.enabled(Rule::RedefinedWhileUnused) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &checker.semantic.bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if shadowed.is_used() {
continue;
}
// If the shadowing binding isn't considered a "redefinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) && checker.settings.dummy_variable_rgx.is_match(name)
{
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
},
binding.range,
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
}
diagnostics.push(diagnostic);
}
}
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedAnnotation) {
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
}
if !checker.is_stub {
if checker.any_enabled(&[
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
Rule::UnusedLambdaArgument,
Rule::UnusedMethodArgument,
Rule::UnusedStaticMethodArgument,
]) {
flake8_unused_arguments::rules::unused_arguments(
checker,
scope,
&mut diagnostics,
);
}
}
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
) {
if enforce_typing_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic
.scopes
.ancestor_ids(scope_id)
.flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter())
.copied()
.collect();
if checker.enabled(Rule::RuntimeImportInTypeCheckingBlock) {
flake8_type_checking::rules::runtime_import_in_type_checking_block(
checker,
scope,
&mut diagnostics,
);
}
if checker.any_enabled(&[
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
]) {
flake8_type_checking::rules::typing_only_runtime_import(
checker,
scope,
&runtime_imports,
&mut diagnostics,
);
}
}
if checker.enabled(Rule::UnusedImport) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -0,0 +1,291 @@
use ruff_python_ast::str::raw_contents_range;
use ruff_text_size::TextRange;
use rustpython_parser::ast::Ranged;
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::docstrings::Docstring;
use crate::fs::relativize_path;
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle};
use crate::{docstrings, warn_user};
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
///
/// This phase is expected to run after the AST has been traversed in its entirety; as such,
/// it is expected that all [`Definition`] nodes have been visited by the time, and that this
/// method will not recurse into any other nodes.
pub(crate) fn definitions(checker: &mut Checker) {
let enforce_annotations = checker.any_enabled(&[
Rule::AnyType,
Rule::MissingReturnTypeClassMethod,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingTypeArgs,
Rule::MissingTypeCls,
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.is_stub
&& checker.any_enabled(&[Rule::DocstringInStub, Rule::IterMethodReturnIterable]);
let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection,
Rule::BlankLineAfterSummary,
Rule::BlankLineBeforeClass,
Rule::BlankLinesBetweenHeaderAndContent,
Rule::CapitalizeSectionName,
Rule::DashedUnderlineAfterSection,
Rule::DocstringStartsWithThis,
Rule::EmptyDocstring,
Rule::EmptyDocstringSection,
Rule::EndsInPeriod,
Rule::EndsInPunctuation,
Rule::EscapeSequenceInDocstring,
Rule::FirstLineCapitalized,
Rule::FitsOnOneLine,
Rule::IndentWithSpaces,
Rule::MultiLineSummaryFirstLine,
Rule::MultiLineSummarySecondLine,
Rule::NewLineAfterLastParagraph,
Rule::NewLineAfterSectionName,
Rule::NoBlankLineAfterFunction,
Rule::NoBlankLineAfterSection,
Rule::NoBlankLineBeforeFunction,
Rule::NoBlankLineBeforeSection,
Rule::NoSignature,
Rule::NonImperativeMood,
Rule::OneBlankLineAfterClass,
Rule::OneBlankLineBeforeClass,
Rule::OverIndentation,
Rule::OverloadWithDocstring,
Rule::SectionNameEndsInColon,
Rule::SectionNotOverIndented,
Rule::SectionUnderlineAfterName,
Rule::SectionUnderlineMatchesSectionLength,
Rule::SectionUnderlineNotOverIndented,
Rule::SurroundingWhitespace,
Rule::TripleSingleQuotes,
Rule::UnderIndentation,
Rule::UndocumentedMagicMethod,
Rule::UndocumentedParam,
Rule::UndocumentedPublicClass,
Rule::UndocumentedPublicFunction,
Rule::UndocumentedPublicInit,
Rule::UndocumentedPublicMethod,
Rule::UndocumentedPublicModule,
Rule::UndocumentedPublicNestedClass,
Rule::UndocumentedPublicPackage,
]);
if !enforce_annotations && !enforce_docstrings && !enforce_stubs {
return;
}
// Compute visibility of all definitions.
let exports: Option<Vec<&str>> = {
checker
.semantic
.global_scope()
.get_all("__all__")
.map(|binding_id| &checker.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => Some(names.iter().copied()),
_ => None,
})
.fold(None, |acc, names| {
Some(acc.into_iter().flatten().chain(names).collect())
})
};
let definitions = std::mem::take(&mut checker.semantic.definitions);
let mut overloaded_name: Option<String> = None;
for ContextualizedDefinition {
definition,
visibility,
} in definitions.resolve(exports.as_deref()).iter()
{
let docstring = docstrings::extraction::extract_docstring(definition);
// flake8-annotations
if enforce_annotations {
// TODO(charlie): This should be even stricter, in that an overload
// implementation should come immediately after the overloaded
// interfaces, without any AST nodes in between. Right now, we
// only error when traversing definition boundaries (functions,
// classes, etc.).
if !overloaded_name.map_or(false, |overloaded_name| {
flake8_annotations::helpers::is_overload_impl(
definition,
&overloaded_name,
&checker.semantic,
)
}) {
checker
.diagnostics
.extend(flake8_annotations::rules::definition(
checker,
definition,
*visibility,
));
}
overloaded_name =
flake8_annotations::helpers::overloaded_name(definition, &checker.semantic);
}
// flake8-pyi
if enforce_stubs {
if checker.enabled(Rule::DocstringInStub) {
flake8_pyi::rules::docstring_in_stubs(checker, docstring);
}
if checker.enabled(Rule::IterMethodReturnIterable) {
flake8_pyi::rules::iter_method_return_iterable(checker, definition);
}
}
// pydocstyle
if enforce_docstrings {
if pydocstyle::helpers::should_ignore_definition(
definition,
&checker.settings.pydocstyle.ignore_decorators,
&checker.semantic,
) {
continue;
}
// Extract a `Docstring` from a `Definition`.
let Some(expr) = docstring else {
pydocstyle::rules::not_missing(checker, definition, *visibility);
continue;
};
let contents = checker.locator.slice(expr.range());
let indentation = checker.locator.slice(TextRange::new(
checker.locator.line_start(expr.start()),
expr.start(),
));
if pydocstyle::helpers::should_ignore_docstring(contents) {
#[allow(deprecated)]
let location = checker.locator.compute_source_location(expr.start());
warn_user!(
"Docstring at {}:{}:{} contains implicit string concatenation; ignoring...",
relativize_path(checker.path),
location.row,
location.column
);
continue;
}
// SAFETY: Safe for docstrings that pass `should_ignore_docstring`.
let body_range = raw_contents_range(contents).unwrap();
let docstring = Docstring {
definition,
expr,
contents,
body_range,
indentation,
};
if !pydocstyle::rules::not_empty(checker, &docstring) {
continue;
}
if checker.enabled(Rule::FitsOnOneLine) {
pydocstyle::rules::one_liner(checker, &docstring);
}
if checker.any_enabled(&[
Rule::NoBlankLineAfterFunction,
Rule::NoBlankLineBeforeFunction,
]) {
pydocstyle::rules::blank_before_after_function(checker, &docstring);
}
if checker.any_enabled(&[
Rule::BlankLineBeforeClass,
Rule::OneBlankLineAfterClass,
Rule::OneBlankLineBeforeClass,
]) {
pydocstyle::rules::blank_before_after_class(checker, &docstring);
}
if checker.enabled(Rule::BlankLineAfterSummary) {
pydocstyle::rules::blank_after_summary(checker, &docstring);
}
if checker.any_enabled(&[
Rule::IndentWithSpaces,
Rule::OverIndentation,
Rule::UnderIndentation,
]) {
pydocstyle::rules::indent(checker, &docstring);
}
if checker.enabled(Rule::NewLineAfterLastParagraph) {
pydocstyle::rules::newline_after_last_paragraph(checker, &docstring);
}
if checker.enabled(Rule::SurroundingWhitespace) {
pydocstyle::rules::no_surrounding_whitespace(checker, &docstring);
}
if checker.any_enabled(&[
Rule::MultiLineSummaryFirstLine,
Rule::MultiLineSummarySecondLine,
]) {
pydocstyle::rules::multi_line_summary_start(checker, &docstring);
}
if checker.enabled(Rule::TripleSingleQuotes) {
pydocstyle::rules::triple_quotes(checker, &docstring);
}
if checker.enabled(Rule::EscapeSequenceInDocstring) {
pydocstyle::rules::backslashes(checker, &docstring);
}
if checker.enabled(Rule::EndsInPeriod) {
pydocstyle::rules::ends_with_period(checker, &docstring);
}
if checker.enabled(Rule::NonImperativeMood) {
pydocstyle::rules::non_imperative_mood(
checker,
&docstring,
&checker.settings.pydocstyle.property_decorators,
);
}
if checker.enabled(Rule::NoSignature) {
pydocstyle::rules::no_signature(checker, &docstring);
}
if checker.enabled(Rule::FirstLineCapitalized) {
pydocstyle::rules::capitalized(checker, &docstring);
}
if checker.enabled(Rule::DocstringStartsWithThis) {
pydocstyle::rules::starts_with_this(checker, &docstring);
}
if checker.enabled(Rule::EndsInPunctuation) {
pydocstyle::rules::ends_with_punctuation(checker, &docstring);
}
if checker.enabled(Rule::OverloadWithDocstring) {
pydocstyle::rules::if_needed(checker, &docstring);
}
if checker.any_enabled(&[
Rule::BlankLineAfterLastSection,
Rule::BlankLinesBetweenHeaderAndContent,
Rule::CapitalizeSectionName,
Rule::DashedUnderlineAfterSection,
Rule::EmptyDocstringSection,
Rule::MultiLineSummaryFirstLine,
Rule::NewLineAfterSectionName,
Rule::NoBlankLineAfterSection,
Rule::NoBlankLineBeforeSection,
Rule::SectionNameEndsInColon,
Rule::SectionNotOverIndented,
Rule::SectionUnderlineAfterName,
Rule::SectionUnderlineMatchesSectionLength,
Rule::SectionUnderlineNotOverIndented,
Rule::UndocumentedParam,
]) {
pydocstyle::rules::sections(
checker,
&docstring,
checker.settings.pydocstyle.convention.as_ref(),
);
}
}
}
}

View File

@@ -0,0 +1,88 @@
use rustpython_parser::ast::{self, ExceptHandler, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::{
flake8_bandit, flake8_blind_except, flake8_bugbear, flake8_builtins, pycodestyle, pylint,
tryceratops,
};
/// Run lint rules over an [`ExceptHandler`] syntax node.
pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &mut Checker) {
match except_handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
name,
body,
range: _,
}) => {
if checker.enabled(Rule::BareExcept) {
if let Some(diagnostic) = pycodestyle::rules::bare_except(
type_.as_deref(),
body,
except_handler,
checker.locator,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::RaiseWithoutFromInsideExcept) {
flake8_bugbear::rules::raise_without_from_inside_except(
checker,
name.as_deref(),
body,
);
}
if checker.enabled(Rule::BlindExcept) {
flake8_blind_except::rules::blind_except(
checker,
type_.as_deref(),
name.as_deref(),
body,
);
}
if checker.enabled(Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
checker,
except_handler,
type_.as_deref(),
body,
checker.settings.flake8_bandit.check_typed_exception,
);
}
if checker.enabled(Rule::TryExceptContinue) {
flake8_bandit::rules::try_except_continue(
checker,
except_handler,
type_.as_deref(),
body,
checker.settings.flake8_bandit.check_typed_exception,
);
}
if checker.enabled(Rule::ExceptWithEmptyTuple) {
flake8_bugbear::rules::except_with_empty_tuple(checker, except_handler);
}
if checker.enabled(Rule::ExceptWithNonExceptionClasses) {
flake8_bugbear::rules::except_with_non_exception_classes(checker, except_handler);
}
if checker.enabled(Rule::ReraiseNoCause) {
tryceratops::rules::reraise_no_cause(checker, body);
}
if checker.enabled(Rule::BinaryOpException) {
pylint::rules::binary_op_exception(checker, except_handler);
}
if let Some(name) = name {
if checker.enabled(Rule::AmbiguousVariableName) {
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_variable_name(name.as_str(), name.range())
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
pub(super) use argument::argument;
pub(super) use arguments::arguments;
pub(super) use bindings::bindings;
pub(super) use comprehension::comprehension;
pub(super) use deferred_for_loops::deferred_for_loops;
pub(super) use deferred_scopes::deferred_scopes;
pub(super) use definitions::definitions;
pub(super) use except_handler::except_handler;
pub(super) use expression::expression;
pub(super) use module::module;
pub(super) use statement::statement;
pub(super) use suite::suite;
pub(super) use unresolved_references::unresolved_references;
mod argument;
mod arguments;
mod bindings;
mod comprehension;
mod deferred_for_loops;
mod deferred_scopes;
mod definitions;
mod except_handler;
mod expression;
mod module;
mod statement;
mod suite;
mod unresolved_references;

View File

@@ -0,0 +1,12 @@
use rustpython_parser::ast::Suite;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::flake8_bugbear;
/// Run lint rules over a module.
pub(crate) fn module(suite: &Suite, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, suite);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
use rustpython_parser::ast::Stmt;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::flake8_pie;
/// Run lint rules over a suite of [`Stmt`] syntax nodes.
pub(crate) fn suite(suite: &[Stmt], checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryPass) {
flake8_pie::rules::no_unnecessary_pass(checker, suite);
}
}

View File

@@ -0,0 +1,47 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_semantic::Exceptions;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::pyflakes;
/// Run lint rules over all [`UnresolvedReference`] entities in the [`SemanticModel`].
pub(crate) fn unresolved_references(checker: &mut Checker) {
if !checker.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) {
return;
}
for reference in checker.semantic.unresolved_references() {
if reference.is_wildcard_import() {
if checker.enabled(Rule::UndefinedLocalWithImportStarUsage) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: reference.name(checker.locator).to_string(),
},
reference.range(),
));
}
} else {
if checker.enabled(Rule::UndefinedName) {
// Avoid flagging if `NameError` is handled.
if reference.exceptions().contains(Exceptions::NAME_ERROR) {
continue;
}
// Allow __path__.
if checker.path.ends_with("__init__.py") {
if reference.name(checker.locator) == "__path__" {
continue;
}
}
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: reference.name(checker.locator).to_string(),
},
reference.range(),
));
}
}
}
}

View File

@@ -14,5 +14,4 @@ pub(crate) struct Deferred<'a> {
pub(crate) functions: Vec<Snapshot>,
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
pub(crate) for_loops: Vec<Snapshot>,
pub(crate) assignments: Vec<Snapshot>,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,21 @@
//! Lint rules based on checking physical lines.
use std::path::Path;
use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_trivia::UniversalNewlines;
use crate::comments::shebang::ShebangDirective;
use crate::registry::Rule;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
use crate::rules::flake8_executable::rules::{
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
};
use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
tab_indentation, trailing_whitespace,
};
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::rules::pylint;
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::Settings;
pub(crate) fn check_physical_lines(
path: &Path,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
@@ -31,15 +23,7 @@ pub(crate) fn check_physical_lines(
settings: &Settings,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let mut has_any_shebang = false;
let enforce_blanket_noqa = settings.rules.enabled(Rule::BlanketNOQA);
let enforce_shebang_not_executable = settings.rules.enabled(Rule::ShebangNotExecutable);
let enforce_shebang_missing = settings.rules.enabled(Rule::ShebangMissingExecutableFile);
let enforce_shebang_whitespace = settings.rules.enabled(Rule::ShebangLeadingWhitespace);
let enforce_shebang_newline = settings.rules.enabled(Rule::ShebangNotFirstLine);
let enforce_shebang_python = settings.rules.enabled(Rule::ShebangMissingPython);
let enforce_blanket_type_ignore = settings.rules.enabled(Rule::BlanketTypeIgnore);
let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong);
let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile);
@@ -53,7 +37,6 @@ pub(crate) fn check_physical_lines(
let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration);
let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
let mut commented_lines_iter = indexer.comment_ranges().iter().peekable();
let mut doc_lines_iter = doc_lines.iter().peekable();
@@ -72,51 +55,6 @@ pub(crate) fn check_physical_lines(
}
}
}
if enforce_blanket_type_ignore {
blanket_type_ignore(&mut diagnostics, &line);
}
if enforce_blanket_noqa {
blanket_noqa(&mut diagnostics, &line);
}
if enforce_shebang_missing
|| enforce_shebang_not_executable
|| enforce_shebang_whitespace
|| enforce_shebang_newline
|| enforce_shebang_python
{
if let Some(shebang) = ShebangDirective::try_extract(&line) {
has_any_shebang = true;
if enforce_shebang_not_executable {
if let Some(diagnostic) =
shebang_not_executable(path, line.range(), &shebang)
{
diagnostics.push(diagnostic);
}
}
if enforce_shebang_whitespace {
if let Some(diagnostic) =
shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace)
{
diagnostics.push(diagnostic);
}
}
if enforce_shebang_newline {
if let Some(diagnostic) =
shebang_newline(line.range(), &shebang, index == 0)
{
diagnostics.push(diagnostic);
}
}
if enforce_shebang_python {
if let Some(diagnostic) = shebang_python(line.range(), &shebang) {
diagnostics.push(diagnostic);
}
}
}
}
}
while doc_lines_iter
@@ -169,12 +107,6 @@ pub(crate) fn check_physical_lines(
}
}
if enforce_shebang_missing && !has_any_shebang {
if let Some(diagnostic) = shebang_missing(path) {
diagnostics.push(diagnostic);
}
}
if enforce_copyright_notice {
if let Some(diagnostic) = missing_copyright_notice(locator, settings) {
diagnostics.push(diagnostic);
@@ -186,8 +118,6 @@ pub(crate) fn check_physical_lines(
#[cfg(test)]
mod tests {
use std::path::Path;
use rustpython_parser::lexer::lex;
use rustpython_parser::Mode;
@@ -209,7 +139,6 @@ mod tests {
let check_with_max_line_length = |line_length: LineLength| {
check_physical_lines(
Path::new("foo.py"),
&locator,
&stylist,
&indexer,

View File

@@ -1,5 +1,7 @@
//! Lint rules based on token traversal.
use std::path::Path;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
@@ -11,83 +13,37 @@ use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule};
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes,
flake8_todos, pycodestyle, pylint, pyupgrade, ruff,
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
};
use crate::settings::Settings;
pub(crate) fn check_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator,
indexer: &Indexer,
tokens: &[LexResult],
settings: &Settings,
is_stub: bool,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let enforce_ambiguous_unicode_character = settings.rules.any_enabled(&[
if settings.rules.enabled(Rule::BlanketNOQA) {
pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator);
}
if settings.rules.enabled(Rule::BlanketTypeIgnore) {
pygrep_hooks::rules::blanket_type_ignore(&mut diagnostics, indexer, locator);
}
if settings.rules.any_enabled(&[
Rule::AmbiguousUnicodeCharacterString,
Rule::AmbiguousUnicodeCharacterDocstring,
Rule::AmbiguousUnicodeCharacterComment,
]);
let enforce_invalid_string_character = settings.rules.any_enabled(&[
Rule::InvalidCharacterBackspace,
Rule::InvalidCharacterSub,
Rule::InvalidCharacterEsc,
Rule::InvalidCharacterNul,
Rule::InvalidCharacterZeroWidthSpace,
]);
let enforce_quotes = settings.rules.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidableEscapedQuote,
]);
let enforce_commented_out_code = settings.rules.enabled(Rule::CommentedOutCode);
let enforce_compound_statements = settings.rules.any_enabled(&[
Rule::MultipleStatementsOnOneLineColon,
Rule::MultipleStatementsOnOneLineSemicolon,
Rule::UselessSemicolon,
]);
let enforce_invalid_escape_sequence = settings.rules.enabled(Rule::InvalidEscapeSequence);
let enforce_implicit_string_concatenation = settings.rules.any_enabled(&[
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]);
let enforce_trailing_comma = settings.rules.any_enabled(&[
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
]);
let enforce_extraneous_parenthesis = settings.rules.enabled(Rule::ExtraneousParentheses);
let enforce_type_comment_in_stub = settings.rules.enabled(Rule::TypeCommentInStub);
// Combine flake8_todos and flake8_fixme so that we can reuse detected [`TodoDirective`]s.
let enforce_todos = settings.rules.any_enabled(&[
Rule::InvalidTodoTag,
Rule::MissingTodoAuthor,
Rule::MissingTodoLink,
Rule::MissingTodoColon,
Rule::MissingTodoDescription,
Rule::InvalidTodoCapitalization,
Rule::MissingSpaceAfterTodoColon,
Rule::LineContainsFixme,
Rule::LineContainsXxx,
Rule::LineContainsTodo,
Rule::LineContainsHack,
]);
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
]) {
let mut state_machine = StateMachine::default();
for &(ref tok, range) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character {
state_machine.consume(tok)
} else {
false
};
let is_docstring = state_machine.consume(tok);
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
ruff::rules::ambiguous_unicode_character(
&mut diagnostics,
@@ -108,13 +64,11 @@ pub(crate) fn check_tokens(
}
}
// ERA001
if enforce_commented_out_code {
if settings.rules.enabled(Rule::CommentedOutCode) {
eradicate::rules::commented_out_code(&mut diagnostics, locator, indexer, settings);
}
// W605
if enforce_invalid_escape_sequence {
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
for (tok, range) in tokens.iter().flatten() {
if tok.is_string() {
pycodestyle::rules::invalid_escape_sequence(
@@ -126,8 +80,14 @@ pub(crate) fn check_tokens(
}
}
}
// PLE2510, PLE2512, PLE2513
if enforce_invalid_string_character {
if settings.rules.any_enabled(&[
Rule::InvalidCharacterBackspace,
Rule::InvalidCharacterSub,
Rule::InvalidCharacterEsc,
Rule::InvalidCharacterNul,
Rule::InvalidCharacterZeroWidthSpace,
]) {
for (tok, range) in tokens.iter().flatten() {
if tok.is_string() {
pylint::rules::invalid_string_characters(&mut diagnostics, *range, locator);
@@ -135,8 +95,11 @@ pub(crate) fn check_tokens(
}
}
// E701, E702, E703
if enforce_compound_statements {
if settings.rules.any_enabled(&[
Rule::MultipleStatementsOnOneLineColon,
Rule::MultipleStatementsOnOneLineSemicolon,
Rule::UselessSemicolon,
]) {
pycodestyle::rules::compound_statements(
&mut diagnostics,
tokens,
@@ -146,13 +109,19 @@ pub(crate) fn check_tokens(
);
}
// Q001, Q002, Q003
if enforce_quotes {
if settings.rules.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidableEscapedQuote,
]) {
flake8_quotes::rules::from_tokens(&mut diagnostics, tokens, locator, settings);
}
// ISC001, ISC002
if enforce_implicit_string_concatenation {
if settings.rules.any_enabled(&[
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]) {
flake8_implicit_str_concat::rules::implicit(
&mut diagnostics,
tokens,
@@ -161,24 +130,45 @@ pub(crate) fn check_tokens(
);
}
// COM812, COM818, COM819
if enforce_trailing_comma {
if settings.rules.any_enabled(&[
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
]) {
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, settings);
}
// UP034
if enforce_extraneous_parenthesis {
if settings.rules.enabled(Rule::ExtraneousParentheses) {
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator, settings);
}
// PYI033
if enforce_type_comment_in_stub && is_stub {
if is_stub && settings.rules.enabled(Rule::TypeCommentInStub) {
flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer);
}
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
// T001, T002, T003, T004
if enforce_todos {
if settings.rules.any_enabled(&[
Rule::ShebangNotExecutable,
Rule::ShebangMissingExecutableFile,
Rule::ShebangLeadingWhitespace,
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]) {
flake8_executable::rules::from_tokens(tokens, path, locator, settings, &mut diagnostics);
}
if settings.rules.any_enabled(&[
Rule::InvalidTodoTag,
Rule::MissingTodoAuthor,
Rule::MissingTodoLink,
Rule::MissingTodoColon,
Rule::MissingTodoDescription,
Rule::InvalidTodoCapitalization,
Rule::MissingSpaceAfterTodoColon,
Rule::LineContainsFixme,
Rule::LineContainsXxx,
Rule::LineContainsTodo,
Rule::LineContainsHack,
]) {
let todo_comments: Vec<TodoComment> = indexer
.comment_ranges()
.iter()
@@ -188,9 +178,7 @@ pub(crate) fn check_tokens(
TodoComment::from_comment(comment, *comment_range, i)
})
.collect();
flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer, settings);
flake8_fixme::rules::todos(&mut diagnostics, &todo_comments);
}

View File

@@ -172,10 +172,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0131") => (RuleGroup::Unspecified, rules::pylint::rules::TypeBivariance),
(Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch),
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
(Pylint, "E0101") => (RuleGroup::Unspecified, rules::pylint::rules::ReturnInInit),
(Pylint, "E0116") => (RuleGroup::Unspecified, rules::pylint::rules::ContinueInFinally),
@@ -214,6 +214,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
(Pylint, "W0131") => (RuleGroup::Unspecified, rules::pylint::rules::NamedExprWithoutContext),
(Pylint, "W0406") => (RuleGroup::Unspecified, rules::pylint::rules::ImportSelf),
@@ -221,6 +222,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W0603") => (RuleGroup::Unspecified, rules::pylint::rules::GlobalStatement),
(Pylint, "W0711") => (RuleGroup::Unspecified, rules::pylint::rules::BinaryOpException),
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
@@ -235,7 +237,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Builtins, "003") => (RuleGroup::Unspecified, rules::flake8_builtins::rules::BuiltinAttributeShadowing),
// flake8-bugbear
(Flake8Bugbear, "002") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnaryPrefixIncrement),
(Flake8Bugbear, "002") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnaryPrefixIncrementDecrement),
(Flake8Bugbear, "003") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::AssignmentToOsEnviron),
(Flake8Bugbear, "004") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnreliableCallableCheck),
(Flake8Bugbear, "005") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::StripWithMultiCharacters),
@@ -652,6 +654,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub),
(Flake8Pyi, "054") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NumericLiteralTooLong),
(Flake8Pyi, "053") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StringOrBytesTooLong),
(Flake8Pyi, "056") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
// flake8-pytest-style
(Flake8PytestStyle, "001") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),
@@ -755,6 +758,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "203") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetatime),
(Flake8UsePathlib, "204") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetmtime),
(Flake8UsePathlib, "205") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetctime),
(Flake8UsePathlib, "206") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsSepSplit),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Unspecified, rules::flake8_logging_format::violations::LoggingStringFormat),

View File

@@ -1,15 +1,10 @@
use ruff_python_trivia::{is_python_whitespace, Cursor};
use ruff_text_size::{TextLen, TextSize};
use std::ops::Deref;
use ruff_python_trivia::Cursor;
/// A shebang directive (e.g., `#!/usr/bin/env python3`).
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ShebangDirective<'a> {
/// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the
/// line.
pub(crate) offset: TextSize,
/// The contents of the directive (e.g., `"/usr/bin/env python3"`).
pub(crate) contents: &'a str,
}
pub(crate) struct ShebangDirective<'a>(&'a str);
impl<'a> ShebangDirective<'a> {
/// Parse a shebang directive from a line, or return `None` if the line does not contain a
@@ -17,9 +12,6 @@ impl<'a> ShebangDirective<'a> {
pub(crate) fn try_extract(line: &'a str) -> Option<Self> {
let mut cursor = Cursor::new(line);
// Trim whitespace.
cursor.eat_while(is_python_whitespace);
// Trim the `#!` prefix.
if !cursor.eat_char('#') {
return None;
@@ -28,10 +20,15 @@ impl<'a> ShebangDirective<'a> {
return None;
}
Some(Self {
offset: line.text_len() - cursor.text_len(),
contents: cursor.chars().as_str(),
})
Some(Self(cursor.chars().as_str()))
}
}
impl Deref for ShebangDirective<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
@@ -59,6 +56,12 @@ mod tests {
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_match_trailing_comment() {
let source = "#!/usr/bin/env python # trailing comment";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_leading_space() {
let source = " #!/usr/bin/env python";

View File

@@ -2,9 +2,4 @@
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(
ShebangDirective {
offset: 4,
contents: "/usr/bin/env python",
},
)
None

View File

@@ -3,8 +3,7 @@ source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(
ShebangDirective {
offset: 2,
contents: "/usr/bin/env python",
},
ShebangDirective(
"/usr/bin/env python",
),
)

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(
ShebangDirective(
"/usr/bin/env python # trailing comment",
),
)

View File

@@ -71,12 +71,12 @@ pub fn extract_directives(
indexer: &Indexer,
) -> Directives {
Directives {
noqa_line_for: if flags.contains(Flags::NOQA) {
noqa_line_for: if flags.intersects(Flags::NOQA) {
extract_noqa_line_for(lxr, locator, indexer)
} else {
NoqaMapping::default()
},
isort: if flags.contains(Flags::ISORT) {
isort: if flags.intersects(Flags::ISORT) {
extract_isort_directives(lxr, locator)
} else {
IsortDirectives::default()

View File

@@ -100,7 +100,9 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_tokens())
{
let is_stub = is_python_stub_file(path);
diagnostics.extend(check_tokens(locator, indexer, &tokens, settings, is_stub));
diagnostics.extend(check_tokens(
&tokens, path, locator, indexer, settings, is_stub,
));
}
// Run the filesystem-based rules.
@@ -193,7 +195,7 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_physical_lines())
{
diagnostics.extend(check_physical_lines(
path, locator, stylist, indexer, &doc_lines, settings,
locator, stylist, indexer, &doc_lines, settings,
));
}

View File

@@ -109,11 +109,11 @@ impl Emitter for TextEmitter {
sep = ":".cyan(),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.flags.contains(EmitterFlags::SHOW_FIX_STATUS)
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS)
}
)?;
if self.flags.contains(EmitterFlags::SHOW_SOURCE) {
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
writeln!(
writer,
"{}",
@@ -124,7 +124,7 @@ impl Emitter for TextEmitter {
)?;
}
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
if self.flags.intersects(EmitterFlags::SHOW_FIX_DIFF) {
if let Some(diff) = Diff::from_message(message) {
writeln!(writer, "{diff}")?;
}

View File

@@ -68,12 +68,8 @@ impl<'a> Directive<'a> {
// If the next character is `:`, then it's a list of codes. Otherwise, it's a directive
// to ignore all rules.
return Ok(Some(
if text[noqa_literal_end..]
.chars()
.next()
.map_or(false, |c| c == ':')
{
let directive = match text[noqa_literal_end..].chars().next() {
Some(':') => {
// E.g., `# noqa: F401, F841`.
let mut codes_start = noqa_literal_end;
@@ -120,8 +116,9 @@ impl<'a> Directive<'a> {
range: range.add(offset),
codes,
})
} else {
// E.g., `# noqa`.
}
None | Some('#') => {
// E.g., `# noqa` or `# noqa# ignore`.
let range = TextRange::new(
TextSize::try_from(comment_start).unwrap(),
TextSize::try_from(noqa_literal_end).unwrap(),
@@ -129,8 +126,21 @@ impl<'a> Directive<'a> {
Self::All(All {
range: range.add(offset),
})
},
));
}
Some(c) if c.is_whitespace() => {
// E.g., `# noqa # ignore`.
let range = TextRange::new(
TextSize::try_from(comment_start).unwrap(),
TextSize::try_from(noqa_literal_end).unwrap(),
);
Self::All(All {
range: range.add(offset),
})
}
_ => return Err(ParseError::InvalidSuffix),
};
return Ok(Some(directive));
}
Ok(None)
@@ -237,7 +247,7 @@ impl FileExemption {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
let path_display = relativize_path(path);
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}");
}
Ok(Some(ParsedFileExemption::All)) => {
return Some(Self::All);
@@ -250,7 +260,8 @@ impl FileExemption {
} else {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
warn!("Invalid code provided to `# ruff: noqa` on line {line}: {code}");
let path_display = relativize_path(path);
warn!("Invalid code provided to `# ruff: noqa` at {path_display}:{line}: {code}");
None
}
}));
@@ -904,6 +915,12 @@ mod tests {
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
}
#[test]
fn noqa_invalid_suffix() {
let source = "# noqa[F401]";
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
}
#[test]
fn flake8_exemption_all() {
let source = "# flake8: noqa";

View File

@@ -238,23 +238,16 @@ impl Rule {
match self {
Rule::InvalidPyprojectToml => LintSource::PyprojectToml,
Rule::UnusedNOQA => LintSource::Noqa,
Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
Rule::BidirectionalUnicode
| Rule::BlankLineWithWhitespace
| Rule::DocLineTooLong
| Rule::LineTooLong
| Rule::MixedSpacesAndTabs
| Rule::MissingNewlineAtEndOfFile
| Rule::UTF8EncodingDeclaration
| Rule::ShebangMissingExecutableFile
| Rule::ShebangNotExecutable
| Rule::ShebangNotFirstLine
| Rule::BidirectionalUnicode
| Rule::ShebangMissingPython
| Rule::ShebangLeadingWhitespace
| Rule::TrailingWhitespace
| Rule::TabIndentation
| Rule::MissingCopyrightNotice
| Rule::BlankLineWithWhitespace => LintSource::PhysicalLines,
| Rule::MissingNewlineAtEndOfFile
| Rule::MixedSpacesAndTabs
| Rule::TabIndentation
| Rule::TrailingWhitespace
| Rule::UTF8EncodingDeclaration => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
@@ -262,34 +255,41 @@ impl Rule {
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::CommentedOutCode
| Rule::MultiLineImplicitStringConcatenation
| Rule::ExtraneousParentheses
| Rule::InvalidCharacterBackspace
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterEsc
| Rule::InvalidCharacterNul
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterZeroWidthSpace
| Rule::ExtraneousParentheses
| Rule::InvalidEscapeSequence
| Rule::SingleLineImplicitStringConcatenation
| Rule::MissingTrailingComma
| Rule::TrailingCommaOnBareTuple
| Rule::MultipleStatementsOnOneLineColon
| Rule::UselessSemicolon
| Rule::MultipleStatementsOnOneLineSemicolon
| Rule::ProhibitedTrailingComma
| Rule::TypeCommentInStub
| Rule::InvalidTodoTag
| Rule::MissingTodoAuthor
| Rule::MissingTodoLink
| Rule::MissingTodoColon
| Rule::MissingTodoDescription
| Rule::InvalidTodoCapitalization
| Rule::MissingSpaceAfterTodoColon
| Rule::InvalidTodoTag
| Rule::LineContainsFixme
| Rule::LineContainsHack
| Rule::LineContainsTodo
| Rule::LineContainsXxx => LintSource::Tokens,
| Rule::LineContainsXxx
| Rule::MissingSpaceAfterTodoColon
| Rule::MissingTodoAuthor
| Rule::MissingTodoColon
| Rule::MissingTodoDescription
| Rule::MissingTodoLink
| Rule::MissingTrailingComma
| Rule::MultiLineImplicitStringConcatenation
| Rule::MultipleStatementsOnOneLineColon
| Rule::MultipleStatementsOnOneLineSemicolon
| Rule::ProhibitedTrailingComma
| Rule::ShebangLeadingWhitespace
| Rule::ShebangMissingExecutableFile
| Rule::ShebangMissingPython
| Rule::ShebangNotExecutable
| Rule::ShebangNotFirstLine
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UselessSemicolon => LintSource::Tokens,
Rule::IOError => LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports,
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem,

View File

@@ -431,18 +431,22 @@ fn is_file_excluded(
#[cfg(test)]
mod tests {
use std::fs::{create_dir, File};
use std::path::Path;
use anyhow::Result;
use globset::GlobSet;
use itertools::Itertools;
use path_absolutize::Absolutize;
use tempfile::TempDir;
use crate::resolver::{
is_file_excluded, match_exclusion, resolve_settings_with_processor, NoOpProcessor,
PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
is_file_excluded, match_exclusion, python_files_in_path, resolve_settings_with_processor,
NoOpProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
};
use crate::settings::pyproject::find_settings_toml;
use crate::settings::types::FilePattern;
use crate::settings::AllSettings;
use crate::test::test_resource_path;
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
@@ -606,4 +610,43 @@ mod tests {
));
Ok(())
}
#[test]
fn find_python_files() -> Result<()> {
// Initialize the filesystem:
// root
// ├── file1.py
// ├── dir1.py
// │ └── file2.py
// └── dir2.py
let tmp_dir = TempDir::new()?;
let root = tmp_dir.path();
let file1 = root.join("file1.py");
let dir1 = root.join("dir1.py");
let file2 = dir1.join("file2.py");
let dir2 = root.join("dir2.py");
File::create(&file1)?;
create_dir(dir1)?;
File::create(&file2)?;
create_dir(dir2)?;
let (paths, _) = python_files_in_path(
&[root.to_path_buf()],
&PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
AllSettings::default(),
None,
),
&NoOpProcessor,
)?;
let paths = paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.sorted()
.collect::<Vec<_>>();
assert_eq!(paths, &[file2, file1]);
Ok(())
}
}

View File

@@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use rustpython_parser::ast::Constant;
use crate::checkers::ast::Checker;
@@ -73,9 +74,7 @@ pub(crate) fn variable_name_task_id(
}
// If the call doesn't have a `task_id` keyword argument, we can't do anything.
let keyword = keywords
.iter()
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "task_id"))?;
let keyword = find_keyword(keywords, "task_id")?;
// If the keyword argument is not a string, we can't do anything.
let task_id = match &keyword.value {

View File

@@ -30,7 +30,7 @@ pub(super) fn match_function_def(
body,
decorator_list,
),
_ => panic!("Found non-FunctionDef in match_name"),
_ => panic!("Found non-FunctionDef in match_function_def"),
}
}

View File

@@ -376,7 +376,7 @@ impl Violation for MissingReturnTypeClassMethod {
}
/// ## What it does
/// Checks that an expression is annotated with a more specific type than
/// Checks that function arguments are annotated with a more specific type than
/// `Any`.
///
/// ## Why is this bad?
@@ -399,9 +399,23 @@ impl Violation for MissingReturnTypeClassMethod {
/// ...
/// ```
///
/// ## Known problems
///
/// Type aliases are unsupported and can lead to false positives.
/// For example, the following will trigger this rule inadvertently:
/// ```python
/// from typing import Any
///
/// MyAny = Any
///
///
/// def foo(x: MyAny):
/// ...
/// ```
///
/// ## References
/// - [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
/// - [`typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
/// - [Mypy: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
#[violation]
pub struct AnyType {
@@ -448,11 +462,12 @@ fn check_dynamically_typed<F>(
}) = annotation
{
// Quoted annotations
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) {
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator())
{
if type_hint_resolves_to_any(
&parsed_annotation,
checker.semantic(),
checker.locator,
checker.locator(),
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
@@ -465,7 +480,7 @@ fn check_dynamically_typed<F>(
if type_hint_resolves_to_any(
annotation,
checker.semantic(),
checker.locator,
checker.locator(),
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
@@ -690,7 +705,7 @@ pub(crate) fn definition(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator, stmt, "None")
fixes::add_return_annotation(checker.locator(), stmt, "None")
.map(Fix::suggested)
});
}
@@ -708,7 +723,7 @@ pub(crate) fn definition(
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator, stmt, return_type)
fixes::add_return_annotation(checker.locator(), stmt, return_type)
.map(Fix::suggested)
});
}

View File

@@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::helpers::CallArguments;
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -61,7 +61,7 @@ pub(crate) fn bad_file_permissions(
matches!(call_path.as_slice(), ["os", "chmod"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
let call_args = CallArguments::new(args, keywords);
if let Some(mode_arg) = call_args.argument("mode", 1) {
if let Some(int_value) = int_value(mode_arg, checker.semantic()) {
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs};
use ruff_python_ast::helpers::{find_keyword, is_const_false, CallArguments};
use crate::checkers::ast::Checker;
@@ -78,15 +78,12 @@ pub(crate) fn hashlib_insecure_hash_functions(
_ => None,
})
{
if !is_used_for_security(keywords) {
return;
}
match hashlib_call {
HashlibCall::New => {
let call_args = SimpleCallArgs::new(args, keywords);
if !is_used_for_security(&call_args) {
return;
}
if let Some(name_arg) = call_args.argument("name", 0) {
if let Some(name_arg) = CallArguments::new(args, keywords).argument("name", 0) {
if let Some(hash_func_name) = string_literal(name_arg) {
// `hashlib.new` accepts both lowercase and uppercase names for hash
// functions.
@@ -105,12 +102,6 @@ pub(crate) fn hashlib_insecure_hash_functions(
}
}
HashlibCall::WeakHash(func_name) => {
let call_args = SimpleCallArgs::new(args, keywords);
if !is_used_for_security(&call_args) {
return;
}
checker.diagnostics.push(Diagnostic::new(
HashlibInsecureHashFunction {
string: (*func_name).to_string(),
@@ -122,13 +113,12 @@ pub(crate) fn hashlib_insecure_hash_functions(
}
}
fn is_used_for_security(call_args: &SimpleCallArgs) -> bool {
match call_args.keyword_argument("usedforsecurity") {
Some(expr) => !is_const_false(expr),
_ => true,
}
fn is_used_for_security(keywords: &[Keyword]) -> bool {
find_keyword(keywords, "usedforsecurity")
.map_or(true, |keyword| !is_const_false(&keyword.value))
}
#[derive(Debug)]
enum HashlibCall {
New,
WeakHash(&'static str),

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use crate::checkers::ast::Checker;
@@ -28,12 +29,7 @@ pub(crate) fn logging_config_insecure_listen(
matches!(call_path.as_slice(), ["logging", "config", "listen"])
})
{
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
if find_keyword(keywords, "verify").is_some() {
return;
}

View File

@@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::helpers::{find_keyword, Truthiness};
use ruff_python_semantic::SemanticModel;
use crate::{
@@ -272,13 +272,10 @@ fn find_shell_keyword<'a>(
keywords: &'a [Keyword],
semantic: &SemanticModel,
) -> Option<ShellKeyword<'a>> {
keywords
.iter()
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "shell"))
.map(|keyword| ShellKeyword {
truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)),
keyword,
})
find_keyword(keywords, "shell").map(|keyword| ShellKeyword {
truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)),
keyword,
})
}
/// Return `true` if the value provided to the `shell` call seems safe. This is based on Bandit's

View File

@@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use crate::checkers::ast::Checker;
@@ -50,12 +51,7 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"])
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "mpModel")
}) {
if let Some(keyword) = find_keyword(keywords, "mpModel") {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::helpers::CallArguments;
use crate::checkers::ast::Checker;
@@ -36,8 +36,7 @@ impl Violation for SnmpWeakCryptography {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is \
insecure."
"You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure."
)
}
}
@@ -56,8 +55,7 @@ pub(crate) fn snmp_weak_cryptography(
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "UsmUserData"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if call_args.len() < 3 {
if CallArguments::new(args, keywords).len() < 3 {
checker
.diagnostics
.push(Diagnostic::new(SnmpWeakCryptography, func.range()));

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::helpers::CallArguments;
use crate::checkers::ast::Checker;
@@ -73,7 +73,7 @@ pub(crate) fn unsafe_yaml_load(
matches!(call_path.as_slice(), ["yaml", "load"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
let call_args = CallArguments::new(args, keywords);
if let Some(loader_arg) = call_args.argument("Loader", 1) {
if !checker
.semantic()

View File

@@ -95,7 +95,11 @@ pub(crate) fn blind_except(
if body.iter().any(|stmt| {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() {
if logging::is_logger_candidate(func, checker.semantic()) {
if logging::is_logger_candidate(
func,
checker.semantic(),
&checker.settings.logger_objects,
) {
if let Some(attribute) = func.as_attribute_expr() {
let attr = attribute.attr.as_str();
if attr == "exception" {

View File

@@ -42,7 +42,7 @@ mod tests {
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))]
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))]
#[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))]
#[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))]
#[test_case(Rule::UnaryPrefixIncrementDecrement, Path::new("B002.py"))]
#[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))]
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))]
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))]

View File

@@ -4,6 +4,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use crate::checkers::ast::Checker;
@@ -115,9 +116,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["pytest", "raises"])
})
&& !keywords
.iter()
.any(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "match"))
&& find_keyword(keywords, "match").is_none()
{
AssertionKind::PytestRaises
} else {

View File

@@ -276,7 +276,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
/// B023
pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &Node<'a>) {
pub(crate) fn function_uses_loop_variable(checker: &mut Checker, node: &Node) {
// Identify any "suspicious" variables. These are defined as variables that are
// referenced in a function or lambda body, but aren't bound as arguments.
let suspicious_variables = {
@@ -303,8 +303,8 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &
// loop, flag it.
for name in suspicious_variables {
if reassigned_in_loop.contains(&name.id.as_str()) {
if !checker.flake8_bugbear_seen.contains(&name) {
checker.flake8_bugbear_seen.push(name);
if !checker.flake8_bugbear_seen.contains(&name.range()) {
checker.flake8_bugbear_seen.push(name.range());
checker.diagnostics.push(Diagnostic::new(
FunctionUsesLoopVariable {
name: name.id.to_string(),

View File

@@ -80,7 +80,7 @@ pub(crate) fn getattr_with_constant(
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
format!("{}.{}", checker.locator.slice(obj.range()), value),
format!("{}.{}", checker.locator().slice(obj.range()), value),
expr.range(),
)));
}

View File

@@ -23,7 +23,7 @@ pub(crate) use reuse_of_groupby_generator::*;
pub(crate) use setattr_with_constant::*;
pub(crate) use star_arg_unpacking_after_keyword_arg::*;
pub(crate) use strip_with_multi_characters::*;
pub(crate) use unary_prefix_increment::*;
pub(crate) use unary_prefix_increment_decrement::*;
pub(crate) use unintentional_type_annotation::*;
pub(crate) use unreliable_callable_check::*;
pub(crate) use unused_loop_control_variable::*;
@@ -57,7 +57,7 @@ mod reuse_of_groupby_generator;
mod setattr_with_constant;
mod star_arg_unpacking_after_keyword_arg;
mod strip_with_multi_characters;
mod unary_prefix_increment;
mod unary_prefix_increment_decrement;
mod unintentional_type_annotation;
mod unreliable_callable_check;
mod unused_loop_control_variable;

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use crate::checkers::ast::Checker;
@@ -48,12 +49,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keyword
return;
}
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "stacklevel")
}) {
if find_keyword(keywords, "stacklevel").is_some() {
return;
}

View File

@@ -1,57 +0,0 @@
use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the unary prefix increment operator (e.g., `++n`).
///
/// ## Why is this bad?
/// Python does not support the unary prefix increment operator. Writing `++n`
/// is equivalent to `+(+(n))`, which is equivalent to `n`.
///
/// ## Example
/// ```python
/// ++n
/// ```
///
/// Use instead:
/// ```python
/// n += 1
/// ```
///
/// ## References
/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)
/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)
#[violation]
pub struct UnaryPrefixIncrement;
impl Violation for UnaryPrefixIncrement {
#[derive_message_formats]
fn message(&self) -> String {
format!("Python does not support the unary prefix increment")
}
}
/// B002
pub(crate) fn unary_prefix_increment(
checker: &mut Checker,
expr: &Expr,
op: UnaryOp,
operand: &Expr,
) {
if !matches!(op, UnaryOp::UAdd) {
return;
}
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else {
return;
};
if !matches!(op, UnaryOp::UAdd) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(UnaryPrefixIncrement, expr.range()));
}

View File

@@ -0,0 +1,87 @@
use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the attempted use of the unary prefix increment (`++`) or
/// decrement operator (`--`).
///
/// ## Why is this bad?
/// Python does not support the unary prefix increment or decrement operator.
/// Writing `++n` is equivalent to `+(+(n))` and writing `--n` is equivalent to
/// `-(-(n))`. In both cases, it is equivalent to `n`.
///
/// ## Example
/// ```python
/// ++x
/// --y
/// ```
///
/// Use instead:
/// ```python
/// x += 1
/// y -= 1
/// ```
///
/// ## References
/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)
/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)
#[violation]
pub struct UnaryPrefixIncrementDecrement {
operator: UnaryPrefixOperatorType,
}
impl Violation for UnaryPrefixIncrementDecrement {
#[derive_message_formats]
fn message(&self) -> String {
let UnaryPrefixIncrementDecrement { operator } = self;
match operator {
UnaryPrefixOperatorType::Increment => {
format!("Python does not support the unary prefix increment operator (`++`)")
}
UnaryPrefixOperatorType::Decrement => {
format!("Python does not support the unary prefix decrement operator (`--`)")
}
}
}
}
/// B002
pub(crate) fn unary_prefix_increment_decrement(
checker: &mut Checker,
expr: &Expr,
op: UnaryOp,
operand: &Expr,
) {
let Expr::UnaryOp(ast::ExprUnaryOp { op: nested_op, .. }) = operand else {
return;
};
match (op, nested_op) {
(UnaryOp::UAdd, UnaryOp::UAdd) => {
checker.diagnostics.push(Diagnostic::new(
UnaryPrefixIncrementDecrement {
operator: UnaryPrefixOperatorType::Increment,
},
expr.range(),
));
}
(UnaryOp::USub, UnaryOp::USub) => {
checker.diagnostics.push(Diagnostic::new(
UnaryPrefixIncrementDecrement {
operator: UnaryPrefixOperatorType::Decrement,
},
expr.range(),
));
}
_ => {}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum UnaryPrefixOperatorType {
Increment,
Decrement,
}

View File

@@ -83,7 +83,7 @@ pub(crate) fn unreliable_callable_check(
if id == "hasattr" {
if checker.semantic().is_builtin("callable") {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("callable({})", checker.locator.slice(obj.range())),
format!("callable({})", checker.locator().slice(obj.range())),
expr.range(),
)));
}

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -51,9 +51,7 @@ pub(crate) fn zip_without_explicit_strict(
if let Expr::Name(ast::ExprName { id, .. }) = func {
if id == "zip"
&& checker.semantic().is_builtin("zip")
&& !kwargs
.iter()
.any(|keyword| keyword.arg.as_ref().map_or(false, |name| name == "strict"))
&& find_keyword(kwargs, "strict").is_none()
&& !args
.iter()
.any(|arg| is_infinite_iterator(arg, checker.semantic()))

View File

@@ -1,19 +1,36 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B002.py:15:9: B002 Python does not support the unary prefix increment
B002.py:18:9: B002 Python does not support the unary prefix increment operator (`++`)
|
14 | def this_is_buggy(n):
15 | x = ++n
17 | def this_is_buggy(n):
18 | x = ++n
| ^^^ B002
16 | return x
19 | y = --n
20 | return x, y
|
B002.py:20:12: B002 Python does not support the unary prefix increment
B002.py:19:9: B002 Python does not support the unary prefix decrement operator (`--`)
|
19 | def this_is_buggy_too(n):
20 | return ++n
17 | def this_is_buggy(n):
18 | x = ++n
19 | y = --n
| ^^^ B002
20 | return x, y
|
B002.py:24:12: B002 Python does not support the unary prefix increment operator (`++`)
|
23 | def this_is_buggy_too(n):
24 | return ++n, --n
| ^^^ B002
|
B002.py:24:17: B002 Python does not support the unary prefix decrement operator (`--`)
|
23 | def this_is_buggy_too(n):
24 | return ++n, --n
| ^^^ B002
|

View File

@@ -1,15 +1,18 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast;
use rustpython_parser::ast::Decorator;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
/// Checks for any class attributes that use the same name as a builtin.
/// Checks for any class attributes or methods that use the same name as a
/// builtin.
///
/// ## Why is this bad?
/// Reusing a builtin name for the name of an attribute increases the
@@ -19,7 +22,9 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
///
/// Builtins can be marked as exceptions to this rule via the
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
/// converted to the appropriate dunder method.
/// converted to the appropriate dunder method. Methods decorated with
/// `@typing.override` or `@typing_extensions.override` are also
/// ignored.
///
/// ## Example
/// ```python
@@ -88,3 +93,59 @@ pub(crate) fn builtin_attribute_shadowing(
));
}
}
/// A003
pub(crate) fn builtin_method_shadowing(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
name: &str,
decorator_list: &[Decorator],
range: TextRange,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
// those should be flagged on the superclass, but that's more difficult.
if is_standard_library_override(name, class_def, checker.semantic()) {
return;
}
// Ignore explicit overrides.
if decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
}
}
/// Return `true` if an attribute appears to be an override of a standard-library method.
fn is_standard_library_override(
name: &str,
class_def: &ast::StmtClassDef,
model: &SemanticModel,
) -> bool {
match name {
// Ex) `Event#set`
"set" => class_def.bases.iter().any(|base| {
model.resolve_call_path(base).map_or(false, |call_path| {
matches!(call_path.as_slice(), ["threading", "Event"])
})
}),
// Ex) `Filter#filter`
"filter" => class_def.bases.iter().any(|base| {
model.resolve_call_path(base).map_or(false, |call_path| {
matches!(call_path.as_slice(), ["logging", "Filter"])
})
}),
_ => false,
}
}

View File

@@ -38,4 +38,31 @@ A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
12 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
|

View File

@@ -19,4 +19,31 @@ A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
12 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
|

View File

@@ -61,8 +61,8 @@ pub(crate) fn fix_unnecessary_generator_set(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
let module_text = locator.slice(expr.range());
@@ -99,8 +99,8 @@ pub(crate) fn fix_unnecessary_generator_dict(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -142,8 +142,8 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(ListComp)))) ->
// Expr(SetComp)))
let module_text = locator.slice(expr.range());
@@ -178,8 +178,8 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -265,8 +265,8 @@ pub(crate) fn fix_unnecessary_literal_set(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(List|Tuple)))) -> Expr(Set)))
let module_text = locator.slice(expr.range());
@@ -309,8 +309,8 @@ pub(crate) fn fix_unnecessary_literal_dict(
checker: &Checker,
expr: &rustpython_parser::ast::Expr,
) -> Result<Edit> {
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(List|Tuple)))) -> Expr(Dict)))
let module_text = locator.slice(expr.range());
@@ -381,8 +381,8 @@ pub(crate) fn fix_unnecessary_collection_call(
Dict,
}
let locator = checker.locator;
let stylist = checker.stylist;
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
let module_text = locator.slice(expr.range());
@@ -511,12 +511,12 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let prefix = checker.locator.up_to(range.start());
let prefix = checker.locator().up_to(range.start());
let left_pad = matches!(prefix.chars().next_back(), Some('{'));
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let suffix = checker.locator.after(range.end());
let suffix = checker.locator().after(range.end());
let right_pad = matches!(suffix.chars().next(), Some('}'));
if left_pad && right_pad {

View File

@@ -86,8 +86,11 @@ pub(crate) fn unnecessary_call_around_sorted(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let edit =
fixes::fix_unnecessary_call_around_sorted(checker.locator, checker.stylist, expr)?;
let edit = fixes::fix_unnecessary_call_around_sorted(
checker.locator(),
checker.stylist(),
expr,
)?;
if outer == "reversed" {
Ok(Fix::suggested(edit))
} else {

View File

@@ -68,7 +68,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_comprehension(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -84,7 +84,11 @@ pub(crate) fn unnecessary_comprehension_any_all(
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_comprehension_any_all(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -135,8 +135,8 @@ pub(crate) fn unnecessary_double_cast_or_process(
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_double_cast_or_process(
checker.locator,
checker.stylist,
checker.locator(),
checker.stylist(),
expr,
)
});

View File

@@ -62,7 +62,7 @@ pub(crate) fn unnecessary_generator_list(
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_generator_list(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -58,7 +58,7 @@ pub(crate) fn unnecessary_list_call(
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_call(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -93,7 +93,11 @@ pub(crate) fn unnecessary_literal_within_dict_call(
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_dict_call(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_literal_within_dict_call(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -96,7 +96,11 @@ pub(crate) fn unnecessary_literal_within_list_call(
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_list_call(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_literal_within_list_call(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -97,7 +97,11 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_literal_within_tuple_call(checker.locator, checker.stylist, expr)
fixes::fix_unnecessary_literal_within_tuple_call(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -163,8 +163,14 @@ pub(crate) fn unnecessary_map(
let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_map(checker.locator, checker.stylist, expr, parent, object_type)
.map(Fix::suggested)
fixes::fix_unnecessary_map(
checker.locator(),
checker.stylist(),
expr,
parent,
object_type,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -65,10 +66,7 @@ pub(crate) fn locals_in_render_function(
return;
}
&args[2]
} else if let Some(keyword) = keywords
.iter()
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "context"))
{
} else if let Some(keyword) = find_keyword(keywords, "context") {
if !is_locals_call(&keyword.value, checker.semantic()) {
return;
}

View File

@@ -188,14 +188,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
Diagnostic::new(RawStringInException, first.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(indentation) =
whitespace::indentation(checker.locator, stmt)
whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist,
checker.stylist(),
checker.generator(),
));
}
@@ -211,14 +211,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(indentation) =
whitespace::indentation(checker.locator, stmt)
whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist,
checker.stylist(),
checker.generator(),
));
}
@@ -238,14 +238,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
Diagnostic::new(DotFormatInException, first.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(indentation) =
whitespace::indentation(checker.locator, stmt)
whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist,
checker.stylist(),
checker.generator(),
));
}

View File

@@ -24,6 +24,7 @@ mod tests {
#[test_case(Path::new("EXE004_1.py"))]
#[test_case(Path::new("EXE004_2.py"))]
#[test_case(Path::new("EXE004_3.py"))]
#[test_case(Path::new("EXE004_4.py"))]
#[test_case(Path::new("EXE005_1.py"))]
#[test_case(Path::new("EXE005_2.py"))]
#[test_case(Path::new("EXE005_3.py"))]

View File

@@ -1,11 +1,60 @@
pub(crate) use shebang_missing::*;
pub(crate) use shebang_newline::*;
pub(crate) use shebang_not_executable::*;
pub(crate) use shebang_python::*;
pub(crate) use shebang_whitespace::*;
use std::path::Path;
mod shebang_missing;
mod shebang_newline;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::Locator;
pub(crate) use shebang_leading_whitespace::*;
pub(crate) use shebang_missing_executable_file::*;
pub(crate) use shebang_missing_python::*;
pub(crate) use shebang_not_executable::*;
pub(crate) use shebang_not_first_line::*;
use crate::comments::shebang::ShebangDirective;
use crate::settings::Settings;
mod shebang_leading_whitespace;
mod shebang_missing_executable_file;
mod shebang_missing_python;
mod shebang_not_executable;
mod shebang_python;
mod shebang_whitespace;
mod shebang_not_first_line;
pub(crate) fn from_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator,
settings: &Settings,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut has_any_shebang = false;
for (tok, range) in tokens.iter().flatten() {
if let Tok::Comment(comment) = tok {
if let Some(shebang) = ShebangDirective::try_extract(comment) {
has_any_shebang = true;
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_executable(path, *range) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator, settings) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
diagnostics.push(diagnostic);
}
}
}
}
if !has_any_shebang {
if let Some(diagnostic) = shebang_missing_executable_file(path) {
diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,11 +1,12 @@
use std::ops::Sub;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use ruff_python_trivia::is_python_whitespace;
use crate::comments::shebang::ShebangDirective;
use crate::registry::AsRule;
use crate::settings::Settings;
/// ## What it does
/// Checks for whitespace before a shebang directive.
@@ -46,31 +47,29 @@ impl AlwaysAutofixableViolation for ShebangLeadingWhitespace {
}
/// EXE004
pub(crate) fn shebang_whitespace(
pub(crate) fn shebang_leading_whitespace(
range: TextRange,
shebang: &ShebangDirective,
autofix: bool,
locator: &Locator,
settings: &Settings,
) -> Option<Diagnostic> {
let ShebangDirective {
offset,
contents: _,
} = shebang;
if *offset > TextSize::from(2) {
let leading_space_start = range.start();
let leading_space_len = offset.sub(TextSize::new(2));
let mut diagnostic = Diagnostic::new(
ShebangLeadingWhitespace,
TextRange::at(leading_space_start, leading_space_len),
);
if autofix {
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at(
leading_space_start,
leading_space_len,
))));
}
Some(diagnostic)
} else {
None
// If the shebang is at the beginning of the file, abort.
if range.start() == TextSize::from(0) {
return None;
}
// If the entire prefix _isn't_ whitespace, abort (this is handled by EXE005).
if !locator
.up_to(range.start())
.chars()
.all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n'))
{
return None;
}
let prefix = TextRange::up_to(range.start());
let mut diagnostic = Diagnostic::new(ShebangLeadingWhitespace, prefix);
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(prefix)));
}
Some(diagnostic)
}

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