Compare commits

...

66 Commits

Author SHA1 Message Date
Charlie Marsh
827747ddcd Merge branch 'main' into simplify-SIM911 2024-01-11 14:32:10 -05:00
trag1c
e871037f7c Made violation logic take ExprCall instead of Expr 2024-01-11 20:03:31 +01:00
trag1c
957aba0be9 Removed redundant loop 2024-01-11 19:58:32 +01:00
trag1c
76e40b2ed3 Corrected variable name 2024-01-11 19:52:01 +01:00
Micha Reiser
f192c72596 Remove type parameter from parse_* methods (#9466) 2024-01-11 19:41:19 +01:00
Charlie Marsh
25bafd2d66 Restrict builtin-attribute-shadowing to actual shadowed references (#9462)
## Summary

This PR attempts to improve `builtin-attribute-shadowing` (`A003`), a
rule which has been repeatedly criticized, but _does_ have value (just
not in the current form).

Historically, this rule would flag cases like:

```python
class Class:
    id: int
```

This led to an increasing number of exceptions and special-cases to the
rule over time to try and improve it's specificity (e.g., ignore
`TypedDict`, ignore `@override`).

The crux of the issue is that given the above, referencing `id` will
never resolve to `Class.id`, so the shadowing is actually fine. There's
one exception, however:

```python
class Class:
    id: int

    def do_thing() -> id:
        pass
```

Here, `id` actually resolves to the `id` attribute on the class, not the
`id` builtin.

So this PR completely reworks the rule around this _much_ more targeted
case, which will almost always be a mistake: when you reference a class
member from within the class, and that member shadows a builtin.

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

Closes https://github.com/astral-sh/ruff/issues/7806.
2024-01-11 12:59:40 -05:00
Randy Syring
7fc51d29c5 docs: fix typo in formatter.md (#9473)
I believe there was a typo in the formatter docs and I've attempted to
fix it according to what I think was originally intended.
2024-01-11 12:59:01 -05:00
Juan Orduz
2d362e9366 Add numpyro and pymc to ruff users (#9468)
- NumPyro: https://github.com/pyro-ppl/numpyro/pull/1700
- PyMC: https://github.com/pymc-devs/pymc/pull/7091
2024-01-11 10:16:15 -05:00
Micha Reiser
501bc1c270 Avoid allocating during implicit concatenated string formatting (#9469) 2024-01-11 15:58:49 +01:00
trag1c
a4c6246ad7 Removed unnecessary preview mode check 2024-01-11 10:50:15 +01:00
manunio
14d3fe6bfa Add a idempotent fuzz_target for ruff_python_formatter (#9448)
Co-authored-by: Addison Crump <addison.crump@cispa.de>
Co-authored-by: Addison Crump <me@addisoncrump.info>
2024-01-11 08:55:59 +01:00
trag1c
7e1309b854 Added newlines 2024-01-11 00:11:46 +01:00
Charlie Marsh
4a3bb67b5f Move pyproject_config into Resolver (#9453)
## Summary

Sort of a random PR to make the coupling between `pyproject_config` and
`resolver` more explicit by passing it to the `Resolver`, rather than
threading it through to each individual method.
2024-01-10 17:58:53 -05:00
trag1c
0ae19efe74 Implemented SIM911 2024-01-10 23:23:35 +01:00
Micha Reiser
79f4abbb8d Import Black's type aliases test (#9456) 2024-01-10 12:37:17 +00:00
Micha Reiser
58fcd96ac1 Update Black Tests (#9455) 2024-01-10 12:09:34 +00:00
Micha Reiser
ac02d3aedd Hug multiline-strings preview style (#9243) 2024-01-10 12:47:34 +01:00
Alex Waygood
6be73322da [RUF021]: Add an autofix (#9449)
## Summary

This adds an autofix for the newly added RUF021 (see #9440).
2024-01-09 17:55:33 +00:00
Charlie Marsh
ad1ca72a35 Add some additional Python 3.12 typing members to deprecated-import (#9445)
Closes https://github.com/astral-sh/ruff/issues/9443.
2024-01-09 12:52:01 -05:00
Charlie Marsh
381811b4a6 Skip extra settings resolution when namespace packages are empty (#9446)
Saves 2% on Airflow:

```shell
❯ hyperfine --warmup 20 -i "./target/release/main format ../airflow" "./target/release/ruff format ../airflow"
Benchmark 1: ./target/release/main format ../airflow
  Time (mean ± σ):      72.7 ms ±   0.4 ms    [User: 48.7 ms, System: 75.5 ms]
  Range (min … max):    72.0 ms …  73.7 ms    40 runs

Benchmark 2: ./target/release/ruff format ../airflow
  Time (mean ± σ):      71.4 ms ±   0.6 ms    [User: 46.2 ms, System: 76.2 ms]
  Range (min … max):    70.3 ms …  73.8 ms    41 runs

Summary
  './target/release/ruff format ../airflow' ran
    1.02 ± 0.01 times faster than './target/release/main format ../airflow'
```
2024-01-09 08:33:22 -05:00
Charlie Marsh
20af5a774f Allow Hashable = None in type annotations (#9442)
Closes https://github.com/astral-sh/ruff/issues/9441.
2024-01-08 22:38:34 -05:00
Alex Waygood
86b1ae9383 Add rule to enforce parentheses in a or b and c (#9440)
Fixes #8721

## Summary

This implements the rule proposed in #8721, as RUF021. `and` always
binds more tightly than `or` when chaining the two together.

(This should definitely be autofixable, but I'm leaving that to a
followup PR for now.)

## Test Plan

`cargo test` / `cargo insta review`
2024-01-08 20:28:03 -05:00
Charlie Marsh
84ab21f073 Add a fix for redefinition-while-unused (#9419)
## Summary

This PR enables Ruff to remove redefined imports, as in:

```python
import os
import os

print(os)
```

Previously, Ruff would flag `F811` on the second `import os`, but
couldn't fix it.

For now, this fix is marked as safe, but only available in preview.

Closes https://github.com/astral-sh/ruff/issues/3477.
2024-01-09 00:29:20 +00:00
Charlie Marsh
985f1d10f6 Don't flag redefined-while-unused in if branches (#9418)
## Summary

On `main`, we flag redefinitions in cases like:

```python
import os

x = 1

if x > 0:
    import os
```

That is, we consider these to be in the "same branch", since they're not
in disjoint branches. This matches Flake8's behavior, but it seems to
lead to false positives.
2024-01-08 17:06:55 -05:00
Charlie Marsh
f419af494f Allow Boolean positionals in setters (#9429)
## Summary

Ignores Boolean trap enforcement for methods that appear to be setters
(as in the Qt and pygame APIs).

Closes https://github.com/astral-sh/ruff/issues/9287.
Closes https://github.com/astral-sh/ruff/issues/8923.
2024-01-08 13:02:16 -05:00
Micha Reiser
94968fedd5 Use Rust 1.75 toolchain (#9437) 2024-01-08 18:03:16 +01:00
Charlie Marsh
ba71772d93 Parenthesize breaking named expressions in match guards (#9396)
## Summary

This is an attempt to solve
https://github.com/astral-sh/ruff/issues/9394 by avoiding breaks in
named expressions when invalid.
2024-01-08 14:47:01 +00:00
Micha Reiser
b1a5df8694 Move locate_cmp_ops to invalid_literal_comparisons (#9438) 2024-01-08 13:15:36 +01:00
dependabot[bot]
0c84782060 Bump tempfile from 3.8.1 to 3.9.0 (#9434)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:57:24 +00:00
dependabot[bot]
d9fc9702cf Bump serde from 1.0.193 to 1.0.195 (#9430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:57:02 +00:00
dependabot[bot]
c58d1aa87d Bump anyhow from 1.0.76 to 1.0.79 (#9432)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:56:55 +00:00
dependabot[bot]
1d1824787b Bump clap from 4.4.12 to 4.4.13 (#9431)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:56:49 +00:00
Charlie Marsh
04afdf177b Disambiguate argument descriptors from section headers (#9427)
## Summary

Given a docstring like:

```python
def func(x: int, args: tuple[int]):
    """Toggle the gizmo.

    Args:
        x: Some argument.
        args: Some other arguments.
    """
```

We were considering the `args:` descriptor to be an indented docstring
section header (since `Args:`) is a valid header name. This led to very
confusing diagnostics.

This PR makes the parsing a bit more lax in this case, such that if we
see a nested header that's more deeply indented than the preceding
header, and the preceding section allows sub-items (like `Args:`), we
avoid treating the nested item as a section header.

Closes https://github.com/astral-sh/ruff/issues/9426.
2024-01-07 22:41:00 -05:00
Alex Waygood
d5a439cbd3 [flake8-pyi] PYI053: Exclude string literals that are the first argument to warnings.deprecated or typing_extensions.deprecated (#9423)
Fixes #9420
2024-01-07 18:41:14 -05:00
Charlie Marsh
63953431a6 Include subscripts and attributes in static key rule (#9416) 2024-01-06 17:28:57 -05:00
Charlie Marsh
f6841757eb Use comment_ranges for isort directive extraction (#9414)
## Summary

No need to iterate over the token stream to find comments -- we already
know where they are.
2024-01-06 16:05:13 -05:00
Charlie Marsh
1666c7a5cb Add size hints to string parser (#9413) 2024-01-06 15:59:34 -05:00
Charlie Marsh
e80b3db10d Remove duplicated NameFinder struct (#9412) 2024-01-06 20:47:28 +00:00
Charlie Marsh
701697c37e Support variable keys in static dictionary key rule (#9411)
Closes https://github.com/astral-sh/ruff/issues/9410.
2024-01-06 20:44:40 +00:00
Charlie Marsh
c2c9997682 Use DisplayParseError for stdin parser errors (#9409)
Just looks like an oversight in refactoring.
2024-01-06 15:28:12 +00:00
Charlie Marsh
cee09765ef Use transformed source code for diagnostic locations (#9408)
## Summary

After we apply fixes, the source code might be transformed. And yet,
we're using the _unmodified_ source code to compute locations in some
cases (e.g., for displaying parse errors, or Jupyter Notebook cells).
This can lead to subtle errors in reporting, or even panics. This PR
modifies the linter to use the _transformed_ source code for such
computations.

Closes https://github.com/astral-sh/ruff/issues/9407.
2024-01-06 10:22:34 -05:00
Alex Waygood
cde4a7d7bf [flake8-pyi] Fix false negative for PYI046 with unused generic protocols (#9405)
I just fixed this false negative in flake8-pyi
(https://github.com/PyCQA/flake8-pyi/pull/460), and then realised ruff
has the exact same bug! Luckily it's a very easy fix.

(The bug is that unused protocols go undetected if they're generic.)
2024-01-05 12:56:04 -06:00
Charlie Marsh
62eca330a8 Remove an unwrap from unnecessary_literal_union.rs (#9404) 2024-01-05 13:19:37 -05:00
Mikael Arguedas
59078c5403 homogenize PLR0914 message to match other PLR 09XX rules and pylint message (#9399) 2024-01-05 07:25:26 -05:00
Jack McIvor
6bf6521197 Fix minor typos (#9402) 2024-01-05 07:24:59 -05:00
qdegraaf
c11f65381f [flake8-bandit] Implement S503 SslWithBadDefaults rule (#9391)
## Summary

Adds S503 rule for the
[flake8-bandit](https://github.com/tylerwince/flake8-bandit) plugin
port.

Checks for function defs argument defaults which have an insecure
ssl_version value. See also
https://bandit.readthedocs.io/en/latest/_modules/bandit/plugins/insecure_ssl_tls.html#ssl_with_bad_defaults

Some logic and the `const` can be shared with
https://github.com/astral-sh/ruff/pull/9390. When one of the two is
merged.

## Test Plan

Fixture added

## Issue Link

Refers: https://github.com/astral-sh/ruff/issues/1646
2024-01-05 01:38:41 +00:00
qdegraaf
6dfc1ccd6f [flake8-bandit] Implement S502 SslInsecureVersion rule (#9390)
## Summary

Adds S502 rule for the
[flake8-bandit](https://github.com/tylerwince/flake8-bandit) plugin
port.

Checks for calls to any function with keywords arguments `ssl_version`
or `method` or for kwargs `method` in calls to `OpenSSL.SSL.Context` and
`ssl_version` in calls to `ssl.wrap_socket` which have an insecure
ssl_version valu. See also
https://bandit.readthedocs.io/en/latest/_modules/bandit/plugins/insecure_ssl_tls.html#ssl_with_bad_version

## Test Plan

Fixture added

## Issue Link

Refers: https://github.com/astral-sh/ruff/issues/1646
2024-01-05 01:27:41 +00:00
Charlie Marsh
60ba7a7c0d Allow # fmt: skip with interspersed same-line comments (#9395)
## Summary

This is similar to https://github.com/astral-sh/ruff/pull/8876, but more
limited in scope:

1. It only applies to `# fmt: skip` (like Black). Like `# isort: on`, `#
fmt: on` needs to be on its own line (still).
2. It only delimits on `#`, so you can do `# fmt: skip # noqa`, but not
`# fmt: skip - some other content` or `# fmt: skip; noqa`.

If we want to support the `;`-delimited version, we should revisit
later, since we don't support that in the linter (so `# fmt: skip; noqa`
wouldn't register a `noqa`).

Closes https://github.com/astral-sh/ruff/issues/8892.
2024-01-04 19:39:37 -05:00
Zanie Blue
4b8b3a1ced Add jupyter notebooks to ecosystem checks (#9293)
Implements https://github.com/astral-sh/ruff/pull/8873 via
https://github.com/astral-sh/ruff/pull/9286
2024-01-04 15:38:42 -06:00
Zanie Blue
967b2dcaf4 Fix ibis ecosystem branch (#9392) 2024-01-04 14:18:08 -05:00
Zanie Blue
aaa00976ae Generate deterministic ids when formatting notebooks (#9359)
When formatting notebooks, we populate the `id` field for cells that do
not have one. Previously, we generated a UUID v4 which resulted in
non-deterministic formatting. Here, we generate the UUID from a seeded
random number generator instead of using true randomness. For example,
here are the first five ids it would generate:

```
7fb27b94-1602-401d-9154-2211134fc71a
acae54e3-7e7d-407b-bb7b-55eff062a284
9a63283c-baf0-4dbc-ab1f-6479b197f3a8
8dd0d809-2fe7-4a7c-9628-1538738b07e2
72eea511-9410-473a-a328-ad9291626812
```

We also add a check that an id is not present in another cell to prevent
accidental introduction of duplicate ids.

The specification is lax, and we could just use incrementing integers
e.g. `0`, `1`, ... but I have a minor preference for retaining the UUID
format. Some discussion
[here](https://github.com/astral-sh/ruff/pull/9359#discussion_r1439607121)
— I'm happy to go either way though.

Discovered via #9293
2024-01-04 09:19:00 -06:00
Charlie Marsh
328262bfac Add cell indexes to all diagnostics (#9387)
## Summary

Ensures that any lint rules that include line locations render them as
relative to the cell (and include the cell number) when inside a Jupyter
notebook.

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

## Test Plan

`cargo test`
2024-01-04 14:02:23 +00:00
Charlie Marsh
f0d43dafcf Ignore trailing quotes for unclosed l-brace errors (#9388)
## Summary

Given:

```python
F"{"ڤ
```

We try to locate the "unclosed left brace" error by subtracting the
quote size from the lexer offset -- so we subtract 1 from the end of the
source, which puts us in the middle of a Unicode character. I don't
think we should try to adjust the offset in this way, since there can be
content _after_ the quote. For example, with the advent of PEP 701, this
string could reasonably be fixed as:

```python
F"{"ڤ"}"
````

Closes https://github.com/astral-sh/ruff/issues/9379.
2024-01-04 05:00:55 +00:00
Charlie Marsh
9a14f403c8 Add missing preview link (#9386) 2024-01-03 19:54:25 -05:00
Noah Jenner
1293383cdc [docs] - Fix admonition hyperlink colouring (#9385)
<!--
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? -->
Fix the colouration of hyperlinks within admonitions on dark theme to be
more readable. Closes #9046

## Test Plan

<!-- How was it tested? -->
Documentation was regenerated via mkdocs and the supplied requirements.

Signed-off-by: 64815328+Eutropios@users.noreply.github.com
2024-01-03 19:41:27 -05:00
qdegraaf
3b323a09cb [flake8-bandit] Add S504 SslWithNoVersion rule (#9384)
## Summary
Adds `S504` rule for the
[flake8-bandit](https://github.com/tylerwince/flake8-bandit) plugin
port.

Checks for calls to `ssl.wrap_socket` which have no `ssl_version`
argument set. See also
https://bandit.readthedocs.io/en/latest/_modules/bandit/plugins/insecure_ssl_tls.html#ssl_with_no_version

## Test Plan

Fixture added 

## Issue Link

Refers: https://github.com/astral-sh/ruff/issues/1646
2024-01-03 21:56:41 +00:00
qdegraaf
5c93a524f1 [flake8-bandit] Implement S4XX suspicious import rules (#8831)
## Summary

Adds all `S4XX` rules to the
[flake8-bandit](https://github.com/tylerwince/flake8-bandit) plugin
port.

There is a lot of documentation to write, some tests can be expanded and
implementation can probably be refactored to be more compact. As there
is some discussion on whether this is actually useful. (See:
https://github.com/astral-sh/ruff/issues/1646#issuecomment-1732331441),
wanted to check which rules we want to have before I go through the
process of polishing this up.

## Test Plan

Fixtures for all rules based on `flake8-bandit`
[tests](https://github.com/tylerwince/flake8-bandit/tree/main/tests)

## Issue link

Refers: https://github.com/astral-sh/ruff/issues/1646
2024-01-03 18:26:26 +00:00
Steve C
e3ad163785 [pylint] Implement unnecessary-dunder-call (C2801) (#9166)
## Summary

Implements
[`C2801`/`unnecessary-dunder-calls`](https://pylint.readthedocs.io/en/stable/user_guide/messages/convention/unnecessary-dunder-call.html)

There are more that this could cover, but the implementations get a
little less straightforward and ugly. Might come back to it in a future
PR, or someone else can!

See: #970 

## Test Plan

`cargo test`
2024-01-03 18:08:37 +00:00
Charlie Marsh
0e202718fd Misc. small tweaks from perusing modules (#9383) 2024-01-03 12:30:25 -05:00
Charlie Marsh
7b6baff734 Respect multi-segment submodule imports when resolving qualified names (#9382)
Ensures that if the user has `import collections.abc`, then
`get_or_import_symbol` returns `collections.abc.Iterator` (or similar)
when requested.
2024-01-03 11:24:20 -05:00
Alex Waygood
1ffc738c84 [flake8-pyi] Add autofix for PYI058 (#9355)
## Summary

This PR adds an autofix for the newly added PYI058 rule (added in
#9313). ~~The PR's current implementation is that the fix is only
available if the fully qualified name of `Generator` or `AsyncGenerator`
is being used:~~
- ~~`-> typing.Generator` is converted to `-> typing.Iterator`;~~
- ~~`-> collections.abc.AsyncGenerator[str, Any]` is converted to `->
collections.abc.AsyncIterator[str]`;~~
- ~~but `-> Generator` is _not_ converted to `-> Iterator`. (It would
require more work to figure out if `Iterator` was already imported or
not. And if it wasn't, where should we import it from? `typing`,
`typing_extensions`, or `collections.abc`? It seems much more
complicated.)~~

The fix is marked as always safe for `__iter__` or `__aiter__` methods
in `.pyi` files, but unsafe for all such methods in `.py` files that
have more than one statement in the method body.

This felt slightly fiddly to accomplish, but I couldn't _see_ any
utilities in
https://github.com/astral-sh/ruff/tree/main/crates/ruff_linter/src/fix
that would have made it simpler to implement. Lmk if I'm missing
something, though -- my first time implementing an autofix! :)

## Test Plan

`cargo test` / `cargo insta review`.
2024-01-03 11:11:16 -05:00
Charlie Marsh
dc5094d42a Handle raises with implicit alternate branches (#9377)
Closes
https://github.com/astral-sh/ruff/issues/9304#issuecomment-1874739740.
2024-01-02 22:59:12 -05:00
Charlie Marsh
fd36754beb Avoid infinite loop in constant vs. None comparisons (#9376)
## Summary

We had an early `continue` in this loop, and we weren't setting
`comparator = next;` when continuing... This PR removes the early
continue altogether for clarity.

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

## Test Plan

`cargo test`
2024-01-02 22:04:52 -05:00
Addison Crump
154d3b9f4b Minor fuzzer improvements (#9375)
<!--
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

- Adds timeouts to fuzzer cmin stages in the case of an infinite loop
- Adds executable flag to reinit-fuzzer.sh because it was annoying me

## Test Plan

Not needed.
2024-01-03 01:52:42 +00:00
Charlie Marsh
6c0734680e Re-enable cargo fuzz in CI (#9372) 2024-01-02 19:45:30 -05:00
Adrian
eac67a9464 Add CERN/Indico to "Who's Using Ruff?" (#9371)
Might be a nice name to have in that list ;)

Corresponding PR that added ruff to the project:
https://github.com/indico/indico/pull/6037
2024-01-02 17:47:22 -05:00
296 changed files with 8239 additions and 2118 deletions

View File

@@ -168,7 +168,7 @@ jobs:
cargo-fuzz:
runs-on: ubuntu-latest
needs: determine_changes
if: false # ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
name: "cargo fuzz"
steps:
- uses: actions/checkout@v4
@@ -180,7 +180,7 @@ jobs:
- name: "Install cargo-fuzz"
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz@0.11
tool: cargo-fuzz@0.11.2
- run: cargo fuzz build -s none
scripts:

View File

@@ -370,7 +370,7 @@ See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/pyt
We have several ways of benchmarking and profiling Ruff:
- Our main performance benchmark comparing Ruff with other tools on the CPython codebase
- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests.
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests.
- Profiling the linter on either the microbenchmarks or entire projects
### CPython Benchmark

104
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.76"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "argfile"
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.12"
version = "4.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d"
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
dependencies = [
"clap_builder",
"clap_derive",
@@ -382,7 +382,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -600,7 +600,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -611,7 +611,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -748,23 +748,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.3"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
"windows-sys 0.52.0",
]
[[package]]
@@ -1107,7 +1096,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -1250,9 +1239,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.149"
version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "libcst"
@@ -1297,9 +1286,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
name = "lock_api"
@@ -1696,7 +1685,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -1781,9 +1770,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.73"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [
"unicode-ident",
]
@@ -1827,9 +1816,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -1996,7 +1985,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2248,7 +2237,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2259,6 +2248,7 @@ dependencies = [
"insta",
"itertools 0.12.0",
"once_cell",
"rand",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -2538,15 +2528,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2652,9 +2642,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
@@ -2672,13 +2662,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2740,7 +2730,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2844,7 +2834,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2860,9 +2850,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.40"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
@@ -2871,15 +2861,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.8.1"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2940,7 +2930,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2952,7 +2942,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"test-case-core",
]
@@ -2973,7 +2963,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3110,7 +3100,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3328,7 +3318,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3422,7 +3412,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"wasm-bindgen-shared",
]
@@ -3456,7 +3446,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3489,7 +3479,7 @@ checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3816,5 +3806,5 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]

View File

@@ -14,14 +14,14 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.76" }
anyhow = { version = "1.0.79" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.8" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.12", features = ["derive"] }
clap = { version = "4.4.13", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
@@ -76,7 +76,7 @@ rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.20" }
serde = { version = "1.0.193", features = ["derive"] }
serde = { version = "1.0.195", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
serde_json = { version = "1.0.109" }
serde_test = { version = "1.0.152" }
@@ -89,7 +89,7 @@ static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.8.1"}
tempfile = { version ="3.9.0"}
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
tikv-jemallocator = { version ="0.5.0"}

View File

@@ -386,6 +386,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- Benchling ([Refac](https://github.com/benchling/refac))
- [Bokeh](https://github.com/bokeh/bokeh)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- CERN ([Indico](https://getindico.io/))
- [DVC](https://github.com/iterative/dvc)
- [Dagger](https://github.com/dagger/dagger)
- [Dagster](https://github.com/dagster-io/dagster)
@@ -417,6 +418,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [NoneBot](https://github.com/nonebot/nonebot2)
- [NumPyro](https://github.com/pyro-ppl/numpyro)
- [ONNX](https://github.com/onnx/onnx)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [PDM](https://github.com/pdm-project/pdm)
@@ -428,6 +430,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PostHog](https://github.com/PostHog/posthog)
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)

View File

@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
&case,
|b, case| {
// Tokenize the source.
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();

View File

@@ -25,10 +25,9 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
use ruff_workspace::resolver::Resolver;
use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`].
@@ -86,6 +85,7 @@ pub(crate) struct Cache {
changes: Mutex<Vec<Change>>,
/// The "current" timestamp used as cache for the updates of
/// [`FileCache::last_seen`]
#[allow(clippy::struct_field_names)]
last_seen_cache: u64,
}
@@ -442,7 +442,7 @@ pub(super) struct CacheMessage {
pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>;
fn persist(self) -> anyhow::Result<()>;
fn persist(self) -> Result<()>;
}
impl<T> PackageCaches for Option<T>
@@ -468,27 +468,17 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
impl<'a> PackageCacheMap<'a> {
pub(crate) fn init(
pyproject_config: &PyprojectConfig,
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
resolver: &Resolver,
) -> Self {
fn init_cache(path: &Path) {
if let Err(e) = cache::init(path) {
if let Err(e) = init(path) {
error!("Failed to initialize cache at {}: {e:?}", path.display());
}
}
match pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => {
init_cache(&pyproject_config.settings.cache_dir);
}
PyprojectDiscoveryStrategy::Hierarchical => {
for settings in
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
{
init_cache(&settings.cache_dir);
}
}
for settings in resolver.settings() {
init_cache(&settings.cache_dir);
}
Self(
@@ -498,7 +488,7 @@ impl<'a> PackageCacheMap<'a> {
.unique()
.par_bridge()
.map(|cache_root| {
let settings = resolver.resolve(cache_root, pyproject_config);
let settings = resolver.resolve(cache_root);
let cache = Cache::open(cache_root.to_path_buf(), settings);
(cache_root, cache)
})

View File

@@ -38,7 +38,6 @@ pub(crate) fn add_noqa(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
let start = Instant::now();
@@ -57,7 +56,7 @@ pub(crate) fn add_noqa(
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,

View File

@@ -57,16 +57,11 @@ pub(crate) fn check(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
// Load the caches.
let caches = if bool::from(cache) {
Some(PackageCacheMap::init(
pyproject_config,
&package_roots,
&resolver,
))
Some(PackageCacheMap::init(&package_roots, &resolver))
} else {
None
};
@@ -81,7 +76,7 @@ pub(crate) fn check(
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(
@@ -128,7 +123,7 @@ pub(crate) fn check(
Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
if settings.linter.rules.enabled(Rule::IOError) {
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics};
@@ -18,20 +18,20 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa,
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
if pyproject_config.settings.file_resolver.force_exclude {
let mut resolver = Resolver::new(pyproject_config);
if resolver.force_exclude() {
if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default());
}
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().linter.exclude)
}) {
if fix_mode.is_apply() {
parrot_stdin()?;
}
@@ -41,13 +41,13 @@ pub(crate) fn check_stdin(
}
let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages)
});
let mut diagnostics = lint_stdin(
filename,
package_root,
stdin,
&pyproject_config.settings,
resolver.base_settings(),
noqa,
fix_mode,
)?;

View File

@@ -25,9 +25,7 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -79,7 +77,7 @@ pub(crate) fn format(
return Ok(ExitStatus::Success);
}
warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
warn_incompatible_formatter_settings(&resolver);
// Discover the package root for each Python file.
let package_roots = resolver.package_roots(
@@ -88,7 +86,6 @@ pub(crate) fn format(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
&pyproject_config,
);
let caches = if cli.no_cache {
@@ -99,11 +96,7 @@ pub(crate) fn format(
#[cfg(debug_assertions)]
crate::warn_user!("Detected debug build without --no-cache.");
Some(PackageCacheMap::init(
&pyproject_config,
&package_roots,
&resolver,
))
Some(PackageCacheMap::init(&package_roots, &resolver))
};
let start = Instant::now();
@@ -118,7 +111,7 @@ pub(crate) fn format(
return None;
};
let settings = resolver.resolve(path, &pyproject_config);
let settings = resolver.resolve(path);
// Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
@@ -723,15 +716,10 @@ impl Display for FormatCommandError {
}
}
pub(super) fn warn_incompatible_formatter_settings(
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
// First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default();
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for setting in resolver.settings() {
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
@@ -760,9 +748,7 @@ pub(super) fn warn_incompatible_formatter_settings(
}
// Next, validate settings-specific incompatibilities.
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for setting in resolver.settings() {
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab()

View File

@@ -6,7 +6,7 @@ use log::error;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -27,24 +27,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
cli.stdin_filename.as_deref(),
)?;
warn_incompatible_formatter_settings(&pyproject_config, None);
let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver);
let mode = FormatMode::from_cli(cli);
if pyproject_config.settings.file_resolver.force_exclude {
if resolver.force_exclude() {
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().formatter.exclude)
}) {
if mode.is_write() {
parrot_stdin()?;
}
@@ -63,12 +62,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
};
// Format the file.
match format_source_code(
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {

View File

@@ -18,6 +18,7 @@ struct Explanation<'a> {
summary: &'a str,
message_formats: &'a [&'a str],
fix: String,
#[allow(clippy::struct_field_names)]
explanation: Option<&'a str>,
preview: bool,
}

View File

@@ -29,7 +29,7 @@ pub(crate) fn show_settings(
bail!("No files found under the given path");
};
let settings = resolver.resolve(&path, pyproject_config);
let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() {

View File

@@ -1,5 +1,6 @@
#![cfg_attr(target_family = "wasm", allow(dead_code))]
use std::borrow::Cow;
use std::fs::File;
use std::io;
use std::ops::{Add, AddAssign};
@@ -273,6 +274,7 @@ pub(crate) fn lint_path(
data: (messages, imports),
error: parse_error,
},
transformed,
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
@@ -301,7 +303,12 @@ pub(crate) fn lint_path(
flags::FixMode::Generate => {}
}
}
(result, fixed)
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(
@@ -313,8 +320,9 @@ pub(crate) fn lint_path(
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, fixed)
(result, transformed, fixed)
}
} else {
let result = lint_only(
@@ -326,8 +334,9 @@ pub(crate) fn lint_path(
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, fixed)
(result, transformed, fixed)
};
let imports = imports.unwrap_or_default();
@@ -335,7 +344,7 @@ pub(crate) fn lint_path(
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
if parse_error.is_none() {
// `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
// and writing the diff to stdout, respectively). If a file has diagnostics, we
// need to avoid reading from and writing to the cache in these modes.
if match fix_mode {
@@ -350,7 +359,7 @@ pub(crate) fn lint_path(
LintCacheData::from_messages(
&messages,
imports.clone(),
source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
transformed.as_ipy_notebook().map(Notebook::index).cloned(),
),
);
}
@@ -360,11 +369,11 @@ pub(crate) fn lint_path(
if let Some(error) = parse_error {
error!(
"{}",
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &source_kind,)
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &transformed)
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
} else {
FxHashMap::default()
@@ -415,6 +424,7 @@ pub(crate) fn lint_stdin(
data: (messages, imports),
error: parse_error,
},
transformed,
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
@@ -443,8 +453,12 @@ pub(crate) fn lint_stdin(
}
flags::FixMode::Generate => {}
}
(result, fixed)
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(
@@ -456,14 +470,15 @@ pub(crate) fn lint_stdin(
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
}
(result, fixed)
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
}
} else {
let result = lint_only(
@@ -475,20 +490,21 @@ pub(crate) fn lint_stdin(
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, fixed)
(result, transformed, fixed)
};
let imports = imports.unwrap_or_default();
if let Some(err) = parse_error {
if let Some(error) = parse_error {
error!(
"Failed to parse {}: {err}",
path.map_or_else(|| "-".into(), fs::relativize_path).bold()
"{}",
DisplayParseError::from_source_kind(error, path.map(Path::to_path_buf), &transformed)
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(
path.map_or_else(|| "-".into(), |path| path.to_string_lossy().to_string()),
notebook.into_index(),

View File

@@ -284,7 +284,8 @@ fn stdin_fix_jupyter() {
"metadata": {},
"outputs": [],
"source": [
"import os"
"import os\n",
"print(1)"
]
},
{
@@ -302,7 +303,8 @@ fn stdin_fix_jupyter() {
"metadata": {},
"outputs": [],
"source": [
"import sys"
"import sys\n",
"print(x)"
]
},
{
@@ -354,8 +356,8 @@ fn stdin_fix_jupyter() {
"nbformat": 4,
"nbformat_minor": 5
}"#), @r###"
success: true
exit_code: 0
success: false
exit_code: 1
----- stdout -----
{
"cells": [
@@ -365,7 +367,9 @@ fn stdin_fix_jupyter() {
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
"metadata": {},
"outputs": [],
"source": []
"source": [
"print(1)"
]
},
{
"cell_type": "markdown",
@@ -381,7 +385,9 @@ fn stdin_fix_jupyter() {
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
"metadata": {},
"outputs": [],
"source": []
"source": [
"print(x)"
]
},
{
"cell_type": "code",
@@ -433,7 +439,8 @@ fn stdin_fix_jupyter() {
"nbformat_minor": 5
}
----- stderr -----
Found 2 errors (2 fixed, 0 remaining).
Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
Found 3 errors (2 fixed, 1 remaining).
"###);
}
@@ -719,6 +726,22 @@ fn stdin_format_jupyter() {
"###);
}
#[test]
fn stdin_parse_error() {
let mut cmd = RuffCheck::default().build();
assert_cmd_snapshot!(cmd
.pass_stdin("from foo import =\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:17: E999 SyntaxError: Unexpected token '='
Found 1 error.
----- stderr -----
error: Failed to parse at 1:17: Unexpected token '='
"###);
}
#[test]
fn show_source() {
let mut cmd = RuffCheck::default().args(["--show-source"]).build();
@@ -743,6 +766,7 @@ fn show_source() {
fn explain_status_codes_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "F401"]));
}
#[test]
fn explain_status_codes_ruf404() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "RUF404"]), @r###"

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff_cli::args::{FormatCommand, LogLevelArgs};
use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
use ruff_cli::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -38,24 +38,24 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
let args_matches = FormatCommand::command()
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, overrides) = arguments.partition();
Ok((cli, overrides))
}
/// Find the [`PyprojectConfig`] to use for formatting.
fn find_pyproject_config(
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
overrides,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml
@@ -64,11 +64,18 @@ fn ruff_check_paths(
FilePattern::Builtin("*.pyi"),
])
.unwrap();
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?;
if paths.is_empty() {
bail!("no python files in {:?}", dirs)
}
Ok((paths, resolver, pyproject_config))
Ok(pyproject_config)
}
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths<'a>(
pyproject_config: &'a PyprojectConfig,
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
Ok((paths, resolver))
}
/// Collects statistics over the formatted files to compute the Jaccard index or the similarity
@@ -216,6 +223,7 @@ pub(crate) struct Args {
#[arg(long)]
pub(crate) files_with_errors: Option<u32>,
#[clap(flatten)]
#[allow(clippy::struct_field_names)]
pub(crate) log_level_args: LogLevelArgs,
}
@@ -451,11 +459,17 @@ fn format_dev_project(
files[0].display()
);
// TODO(konstin): black excludes
// TODO(konstin): Respect black's excludes.
// Find files to check (or in this case, format twice). Adapted from ruff_cli
// First argument is ignored
let (paths, resolver, pyproject_config) = ruff_check_paths(files)?;
let (cli, overrides) = parse_cli(files)?;
let pyproject_config = find_pyproject_config(&cli, &overrides)?;
let (paths, resolver) = ruff_check_paths(&pyproject_config, &cli, &overrides)?;
if paths.is_empty() {
bail!("No Python files found under the given path(s)");
}
let results = {
let pb_span =
@@ -468,14 +482,7 @@ fn format_dev_project(
#[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() };
iter.map(|path| {
let result = format_dir_entry(
path,
stability_check,
write,
&black_options,
&resolver,
&pyproject_config,
);
let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
pb_span.pb_inc(1);
result
})
@@ -525,14 +532,13 @@ fn format_dev_project(
})
}
/// Error handling in between walkdir and `format_dev_file`
/// Error handling in between walkdir and `format_dev_file`.
fn format_dir_entry(
resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool,
write: bool,
options: &BlackOptions,
resolver: &Resolver,
pyproject_config: &PyprojectConfig,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning
@@ -543,7 +549,7 @@ fn format_dir_entry(
let path = resolved_file.into_path();
let mut options = options.to_py_format_options(&path);
let settings = resolver.resolve(&path, pyproject_config);
let settings = resolver.resolve(&path);
// That's a bad way of doing this but it's not worth doing something better for format_dev
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);

View File

@@ -1472,6 +1472,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
fits.state.line_width > fits.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
}
let indent = std::mem::take(&mut self.state.pending_indent);
self.state.line_width +=
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
@@ -1493,7 +1498,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
return Fits::No;
}
match args.measure_mode() {
MeasureMode::FirstLine => return Fits::Yes,
MeasureMode::FirstLine => {
return if exceeds_width(self, args) {
Fits::No
} else {
Fits::Yes
};
}
MeasureMode::AllLines
| MeasureMode::AllLinesAllowTextOverflow => {
self.state.line_width = 0;
@@ -1511,9 +1522,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
}
if self.state.line_width > self.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
{
if exceeds_width(self, args) {
return Fits::No;
}

View File

@@ -264,3 +264,41 @@ def func(x: int):
if x > 0:
return 1
raise ValueError
def func(x: int):
if x > 5:
raise ValueError
else:
pass
def func(x: int):
if x > 5:
raise ValueError
elif x > 10:
pass
def func(x: int):
if x > 5:
raise ValueError
elif x > 10:
return 5
def func():
try:
return 5
except:
pass
raise ValueError
def func(x: int):
match x:
case [1, 2, 3]:
return 1
case y:
return "foo"

View File

@@ -0,0 +1,2 @@
import telnetlib # S401
from telnetlib import Telnet # S401

View File

@@ -0,0 +1,2 @@
import ftplib # S402
from ftplib import FTP # S402

View File

@@ -0,0 +1,8 @@
import dill # S403
from dill import objects # S403
import shelve
from shelve import open
import cPickle
from cPickle import load
import pickle
from pickle import load

View File

@@ -0,0 +1,3 @@
import subprocess # S404
from subprocess import Popen # S404
from subprocess import Popen as pop # S404

View File

@@ -0,0 +1,4 @@
import xml.etree.cElementTree # S405
from xml.etree import cElementTree # S405
import xml.etree.ElementTree # S405
from xml.etree import ElementTree # S405

View File

@@ -0,0 +1,3 @@
from xml import sax # S406
import xml.sax as xmls # S406
import xml.sax # S406

View File

@@ -0,0 +1,2 @@
from xml.dom import expatbuilder # S407
import xml.dom.expatbuilder # S407

View File

@@ -0,0 +1,2 @@
from xml.dom.minidom import parseString # S408
import xml.dom.minidom # S408

View File

@@ -0,0 +1,2 @@
from xml.dom.pulldom import parseString # S409
import xml.dom.pulldom # S409

View File

@@ -0,0 +1,2 @@
import lxml # S410
from lxml import etree # S410

View File

@@ -0,0 +1,2 @@
import xmlrpc # S411
from xmlrpc import server # S411

View File

@@ -0,0 +1 @@
from twisted.web.twcgi import CGIScript # S412

View File

@@ -0,0 +1,4 @@
import Crypto.Hash # S413
from Crypto.Hash import MD2 # S413
import Crypto.PublicKey # S413
from Crypto.PublicKey import RSA # S413

View File

@@ -0,0 +1,3 @@
import pyghmi # S415
from pyghmi import foo # S415

View File

@@ -0,0 +1,16 @@
import ssl
from ssl import wrap_socket
from OpenSSL import SSL
from OpenSSL.SSL import Context
wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
SSL.Context(method=SSL.SSLv2_METHOD) # S502
SSL.Context(method=SSL.SSLv23_METHOD) # S502
Context(method=SSL.SSLv3_METHOD) # S502
Context(method=SSL.TLSv1_METHOD) # S502
wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
SSL.Context(method=SSL.TLS_SERVER_METHOD) # OK
func(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK

View File

@@ -0,0 +1,23 @@
import ssl
from OpenSSL import SSL
from ssl import PROTOCOL_TLSv1
def func(version=ssl.PROTOCOL_SSLv2): # S503
pass
def func(protocol=SSL.SSLv2_METHOD): # S503
pass
def func(version=SSL.SSLv23_METHOD): # S503
pass
def func(protocol=PROTOCOL_TLSv1): # S503
pass
def func(version=SSL.TLSv1_2_METHOD): # OK
pass

View File

@@ -0,0 +1,15 @@
import ssl
from ssl import wrap_socket
ssl.wrap_socket() # S504
wrap_socket() # S504
ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
class Class:
def wrap_socket(self):
pass
obj = Class()
obj.wrap_socket() # OK

View File

@@ -71,6 +71,8 @@ foo.is_(True)
bar.is_not(False)
next(iter([]), False)
sa.func.coalesce(tbl.c.valid, False)
setVisible(True)
set_visible(True)
class Registry:
@@ -114,3 +116,6 @@ from typing import override
@override
def func(x: bool):
pass
settings(True)

View File

@@ -8,46 +8,14 @@ class MyClass:
self.id = 10
self.dir = "."
def str(self):
pass
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
def str(self):
pass
def method_usage(self) -> str:
pass
def attribute_usage(self) -> id:
pass

View File

@@ -1,5 +1,5 @@
import typing
from typing import Protocol
from typing import Protocol, TypeVar
class _Foo(Protocol):
@@ -10,9 +10,23 @@ class _Bar(typing.Protocol):
bar: int
_T = TypeVar("_T")
class _Baz(Protocol[_T]):
x: _T
# OK
class _UsedPrivateProtocol(Protocol):
bar: int
def uses__UsedPrivateProtocol(arg: _UsedPrivateProtocol) -> None: ...
# Also OK
class _UsedGenericPrivateProtocol(Protocol[_T]):
x: _T
def uses_some_private_protocols(
arg: _UsedPrivateProtocol, arg2: _UsedGenericPrivateProtocol[int]
) -> None: ...

View File

@@ -1,5 +1,5 @@
import typing
from typing import Protocol
from typing import Protocol, TypeVar
class _Foo(object, Protocol):
@@ -10,9 +10,23 @@ class _Bar(typing.Protocol):
bar: int
_T = TypeVar("_T")
class _Baz(Protocol[_T]):
x: _T
# OK
class _UsedPrivateProtocol(Protocol):
bar: int
def uses__UsedPrivateProtocol(arg: _UsedPrivateProtocol) -> None: ...
# Also OK
class _UsedGenericPrivateProtocol(Protocol[_T]):
x: _T
def uses_some_private_protocols(
arg: _UsedPrivateProtocol, arg2: _UsedGenericPrivateProtocol[int]
) -> None: ...

View File

@@ -1,3 +1,9 @@
import warnings
import typing_extensions
from collections.abc import Callable
from warnings import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None:
...
@@ -45,3 +51,24 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding."""
@warnings.deprecated("Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay")
def deprecated_function() -> None: ...
@typing_extensions.deprecated("Another loooooooooooooooooooooong deprecation message, it's still okay")
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message")
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
def not_a_deprecated_function() -> None: ...

View File

@@ -1,3 +1,7 @@
import warnings
import typing_extensions
from typing_extensions import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
def f2(
x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
@@ -38,3 +42,25 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding.""" # OK
@warnings.deprecated(
"Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay" # OK
)
def deprecated_function() -> None: ...
@typing_extensions.deprecated(
"Another loooooooooooooooooooooong deprecation message, it's still okay" # OK
)
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message") # OK
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
)
def not_a_deprecated_function() -> None: ...

View File

@@ -1,82 +1,174 @@
import collections.abc
import typing
from collections.abc import AsyncGenerator, Generator
from typing import Any
def scope():
from collections.abc import Generator
class IteratorReturningSimpleGenerator1:
def __iter__(self) -> Generator: # PYI058 (use `Iterator`)
return (x for x in range(42))
class IteratorReturningSimpleGenerator1:
def __iter__(self) -> Generator:
... # PYI058 (use `Iterator`)
class IteratorReturningSimpleGenerator2:
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: # PYI058 (use `Iterator`)
"""Fully documented, because I'm a runtime function!"""
yield from "abcdefg"
return None
class IteratorReturningSimpleGenerator3:
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: # PYI058 (use `Iterator`)
yield "a"
yield "b"
yield "c"
return
def scope():
import typing
class AsyncIteratorReturningSimpleAsyncGenerator1:
def __aiter__(self) -> typing.AsyncGenerator: pass # PYI058 (Use `AsyncIterator`)
class IteratorReturningSimpleGenerator2:
def __iter__(self) -> typing.Generator:
... # PYI058 (use `Iterator`)
class AsyncIteratorReturningSimpleAsyncGenerator2:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]: ... # PYI058 (Use `AsyncIterator`)
class AsyncIteratorReturningSimpleAsyncGenerator3:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: pass # PYI058 (Use `AsyncIterator`)
def scope():
import collections.abc
class CorrectIterator:
def __iter__(self) -> Iterator[str]: ... # OK
class IteratorReturningSimpleGenerator3:
def __iter__(self) -> collections.abc.Generator:
... # PYI058 (use `Iterator`)
class CorrectAsyncIterator:
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
class Fine:
def __iter__(self) -> typing.Self: ... # OK
def scope():
import collections.abc
from typing import Any
class StrangeButWeWontComplainHere:
def __aiter__(self) -> list[bytes]: ... # OK
class IteratorReturningSimpleGenerator4:
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]:
... # PYI058 (use `Iterator`)
def __iter__(self) -> Generator: ... # OK (not in class scope)
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
class IteratorReturningComplexGenerator:
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
def scope():
import collections.abc
import typing
class AsyncIteratorReturningComplexAsyncGenerator:
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
class IteratorReturningSimpleGenerator5:
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]:
... # PYI058 (use `Iterator`)
class ClassWithInvalidAsyncAiterMethod:
async def __aiter__(self) -> AsyncGenerator: ... # OK
class IteratorWithUnusualParameters1:
def __iter__(self, foo) -> Generator: ... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters2:
def __iter__(self, *, bar) -> Generator: ... # OK
class IteratorReturningSimpleGenerator6:
def __iter__(self, /) -> Generator[str, None, None]:
... # PYI058 (use `Iterator`)
class IteratorWithUnusualParameters3:
def __iter__(self, *args) -> Generator: ... # OK
class IteratorWithUnusualParameters4:
def __iter__(self, **kwargs) -> Generator: ... # OK
def scope():
import typing_extensions
class IteratorWithIterMethodThatReturnsThings:
def __iter__(self) -> Generator: # OK
yield
return 42
class AsyncIteratorReturningSimpleAsyncGenerator1:
def __aiter__(
self,
) -> typing_extensions.AsyncGenerator:
... # PYI058 (Use `AsyncIterator`)
class IteratorWithIterMethodThatReceivesThingsFromSend:
def __iter__(self) -> Generator: # OK
x = yield 42
class IteratorWithNonTrivialIterBody:
def __iter__(self) -> Generator: # OK
foo, bar, baz = (1, 2, 3)
yield foo
yield bar
yield baz
def scope():
import collections.abc
class AsyncIteratorReturningSimpleAsyncGenerator2:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]:
... # PYI058 (Use `AsyncIterator`)
def scope():
import collections.abc
class AsyncIteratorReturningSimpleAsyncGenerator3:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]:
... # PYI058 (Use `AsyncIterator`)
def scope():
from typing import Iterator
class CorrectIterator:
def __iter__(self) -> Iterator[str]:
... # OK
def scope():
import collections.abc
class CorrectAsyncIterator:
def __aiter__(self) -> collections.abc.AsyncIterator[int]:
... # OK
def scope():
import typing
class Fine:
def __iter__(self) -> typing.Self:
... # OK
def scope():
class StrangeButWeWontComplainHere:
def __aiter__(self) -> list[bytes]:
... # OK
def scope():
from collections.abc import Generator
def __iter__(self) -> Generator:
... # OK (not in class scope)
def scope():
from collections.abc import AsyncGenerator
def __aiter__(self) -> AsyncGenerator:
... # OK (not in class scope)
def scope():
from collections.abc import Generator
class IteratorReturningComplexGenerator:
def __iter__(self) -> Generator[str, int, bytes]:
... # OK
def scope():
from collections.abc import AsyncGenerator
class AsyncIteratorReturningComplexAsyncGenerator:
def __aiter__(self) -> AsyncGenerator[str, int]:
... # OK
def scope():
from collections.abc import AsyncGenerator
class ClassWithInvalidAsyncAiterMethod:
async def __aiter__(self) -> AsyncGenerator:
... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters1:
def __iter__(self, foo) -> Generator:
... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters2:
def __iter__(self, *, bar) -> Generator:
... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters3:
def __iter__(self, *args) -> Generator:
... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters4:
def __iter__(self, **kwargs) -> Generator:
... # OK

View File

@@ -1,58 +1,128 @@
import collections.abc
import typing
from collections.abc import AsyncGenerator, Generator
from typing import Any
def scope():
from collections.abc import Generator
class IteratorReturningSimpleGenerator1:
def __iter__(self) -> Generator: ... # PYI058 (use `Iterator`)
class IteratorReturningSimpleGenerator1:
def __iter__(self) -> Generator: ... # PYI058 (use `Iterator`)
class IteratorReturningSimpleGenerator2:
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: ... # PYI058 (use `Iterator`)
def scope():
import typing
class IteratorReturningSimpleGenerator3:
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: ... # PYI058 (use `Iterator`)
class IteratorReturningSimpleGenerator2:
def __iter__(self) -> typing.Generator: ... # PYI058 (use `Iterator`)
class AsyncIteratorReturningSimpleAsyncGenerator1:
def __aiter__(self) -> typing.AsyncGenerator: ... # PYI058 (Use `AsyncIterator`)
def scope():
import collections.abc
class AsyncIteratorReturningSimpleAsyncGenerator2:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, Any]: ... # PYI058 (Use `AsyncIterator`)
class IteratorReturningSimpleGenerator3:
def __iter__(self) -> collections.abc.Generator: ... # PYI058 (use `Iterator`)
class AsyncIteratorReturningSimpleAsyncGenerator3:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: ... # PYI058 (Use `AsyncIterator`)
def scope():
import collections.abc
from typing import Any
class CorrectIterator:
def __iter__(self) -> Iterator[str]: ... # OK
class IteratorReturningSimpleGenerator4:
def __iter__(self, /) -> collections.abc.Generator[str, Any, None]: ... # PYI058 (use `Iterator`)
class CorrectAsyncIterator:
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
def scope():
import collections.abc
import typing
class Fine:
def __iter__(self) -> typing.Self: ... # OK
class IteratorReturningSimpleGenerator5:
def __iter__(self, /) -> collections.abc.Generator[str, None, typing.Any]: ... # PYI058 (use `Iterator`)
class StrangeButWeWontComplainHere:
def __aiter__(self) -> list[bytes]: ... # OK
def scope():
from collections.abc import Generator
def __iter__(self) -> Generator: ... # OK (not in class scope)
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
class IteratorReturningSimpleGenerator6:
def __iter__(self, /) -> Generator[str, None, None]: ... # PYI058 (use `Iterator`)
class IteratorReturningComplexGenerator:
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
def scope():
import typing_extensions
class AsyncIteratorReturningComplexAsyncGenerator:
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
class AsyncIteratorReturningSimpleAsyncGenerator1:
def __aiter__(self,) -> typing_extensions.AsyncGenerator: ... # PYI058 (Use `AsyncIterator`)
class ClassWithInvalidAsyncAiterMethod:
async def __aiter__(self) -> AsyncGenerator: ... # OK
def scope():
import collections.abc
class IteratorWithUnusualParameters1:
def __iter__(self, foo) -> Generator: ... # OK
class AsyncIteratorReturningSimpleAsyncGenerator3:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]:
... # PYI058 (Use `AsyncIterator`)
class IteratorWithUnusualParameters2:
def __iter__(self, *, bar) -> Generator: ... # OK
def scope():
import collections.abc
class IteratorWithUnusualParameters3:
def __iter__(self, *args) -> Generator: ... # OK
class AsyncIteratorReturningSimpleAsyncGenerator3:
def __aiter__(self, /) -> collections.abc.AsyncGenerator[str, None]: ... # PYI058 (Use `AsyncIterator`)
class IteratorWithUnusualParameters4:
def __iter__(self, **kwargs) -> Generator: ... # OK
def scope():
from typing import Iterator
class CorrectIterator:
def __iter__(self) -> Iterator[str]: ... # OK
def scope():
import collections.abc
class CorrectAsyncIterator:
def __aiter__(self) -> collections.abc.AsyncIterator[int]: ... # OK
def scope():
import typing
class Fine:
def __iter__(self) -> typing.Self: ... # OK
def scope():
class StrangeButWeWontComplainHere:
def __aiter__(self) -> list[bytes]: ... # OK
def scope():
from collections.abc import Generator
def __iter__(self) -> Generator: ... # OK (not in class scope)
def scope():
from collections.abc import AsyncGenerator
def __aiter__(self) -> AsyncGenerator: ... # OK (not in class scope)
def scope():
from collections.abc import Generator
class IteratorReturningComplexGenerator:
def __iter__(self) -> Generator[str, int, bytes]: ... # OK
def scope():
from collections.abc import AsyncGenerator
class AsyncIteratorReturningComplexAsyncGenerator:
def __aiter__(self) -> AsyncGenerator[str, int]: ... # OK
def scope():
from collections.abc import AsyncGenerator
class ClassWithInvalidAsyncAiterMethod:
async def __aiter__(self) -> AsyncGenerator: ... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters1:
def __iter__(self, foo) -> Generator: ... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters2:
def __iter__(self, *, bar) -> Generator: ... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters3:
def __iter__(self, *args) -> Generator: ... # OK
def scope():
from collections.abc import Generator
class IteratorWithUnusualParameters4:
def __iter__(self, **kwargs) -> Generator: ... # OK

View File

@@ -0,0 +1,23 @@
def foo(d: dict[str, str]) -> None:
for k, v in zip(d.keys(), d.values()): # SIM911
...
for k, v in zip(d.keys(), d.values(), strict=True): # SIM911
...
for k, v in zip(d.keys(), d.values(), struct=True): # OK
...
d1 = d2 = {}
for k, v in zip(d1.keys(), d2.values()): # OK
...
for k, v in zip(d1.items(), d2.values()): # OK
...
for k, v in zip(d2.keys(), d2.values()): # SIM911
...
items = zip(x.keys(), x.values()) # OK

View File

@@ -51,3 +51,5 @@ if (True) == TrueElement or x == TrueElement:
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar
assert x in c > 0 == None

View File

@@ -150,3 +150,21 @@ class Test:
Args:
arg1: some description of arg
"""
def select_data(
query: str,
args: tuple,
database: str,
auto_save: bool,
) -> None:
"""This function has an argument `args`, which shouldn't be mistaken for a section.
Args:
query:
Query template.
args:
A list of arguments.
database:
Which database to connect to ("origin" or "destination").
"""

View File

@@ -536,9 +536,29 @@ def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""
def lowercase_sub_section_header():
"""Below, `returns:` should _not_ be considered a section header.
Args:
Here's a note.
returns:
"""
def titlecase_sub_section_header():
"""Below, `Returns:` should be considered a section header.
Args:
Here's a note.
Returns:
"""

View File

@@ -0,0 +1,60 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import os.path as path"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "481fb4bf-c1b9-47da-927f-3cfdfe4b49ec",
"metadata": {},
"outputs": [],
"source": [
"for os in range(3):\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"for path in range(3):\n",
" pass"
],
"metadata": {
"collapsed": false
},
"id": "2f0c65a5-0a0e-4080-afce-5a8ed0d706df"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff-playground)",
"language": "python",
"name": "ruff-playground"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,6 @@
import os
x = 1
if x > 0:
import os

View File

@@ -0,0 +1,30 @@
from typing import Any
print((3.0).__add__(4.0)) # PLC2801
print((3.0).__sub__(4.0)) # PLC2801
print((3.0).__mul__(4.0)) # PLC2801
print((3.0).__truediv__(4.0)) # PLC2801
print((3.0).__floordiv__(4.0)) # PLC2801
print((3.0).__mod__(4.0)) # PLC2801
print((3.0).__eq__(4.0)) # PLC2801
print((3.0).__ne__(4.0)) # PLC2801
print((3.0).__lt__(4.0)) # PLC2801
print((3.0).__le__(4.0)) # PLC2801
print((3.0).__gt__(4.0)) # PLC2801
print((3.0).__ge__(4.0)) # PLC2801
print((3.0).__str__()) # PLC2801
print((3.0).__repr__()) # PLC2801
print([1, 2, 3].__len__()) # PLC2801
print((1).__neg__()) # PLC2801
class Thing:
def __init__(self, stuff: Any) -> None:
super().__init__() # OK
super().__class__(stuff=(1, 2, 3)) # OK
blah = lambda: {"a": 1}.__delitem__("a") # OK
blah = dict[{"a": 1}.__delitem__("a")] # OK

View File

@@ -91,3 +91,12 @@ from typing_extensions import dataclass_transform
# UP035
from backports.strenum import StrEnum
# UP035
from typing_extensions import override
# UP035
from typing_extensions import Buffer
# UP035
from typing_extensions import get_original_bases

View File

@@ -1,13 +1,22 @@
data = ["some", "Data"]
constant = 5
# Ok
# OK
{value: value.upper() for value in data}
{value.lower(): value.upper() for value in data}
{v: v*v for v in range(10)}
{(0, "a", v): v*v for v in range(10)} # Tuple with variable
{v: v * v for v in range(10)}
{(0, "a", v): v * v for v in range(10)} # Tuple with variable
{constant: value.upper() for value in data for constant in data}
{value.attribute: value.upper() for value in data for constant in data}
{constant[value]: value.upper() for value in data for constant in data}
{value[constant]: value.upper() for value in data for constant in data}
# Errors
{"key": value.upper() for value in data}
{True: value.upper() for value in data}
{0: value.upper() for value in data}
{(1, "a"): value.upper() for value in data} # constant tuple
{(1, "a"): value.upper() for value in data} # Constant tuple
{constant: value.upper() for value in data}
{constant + constant: value.upper() for value in data}
{constant.attribute: value.upper() for value in data}
{constant[0]: value.upper() for value in data}

View File

@@ -1,5 +1,5 @@
import typing
from typing import Annotated, Any, Literal, Optional, Tuple, Union
from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
def f(arg: int):
@@ -257,3 +257,13 @@ from custom_typing import MaybeInt
def f(arg: MaybeInt = None):
pass
# Hashable
def f(arg: Hashable = None): # OK
pass
def f(arg: Hashable | int = None): # OK
pass

View File

@@ -0,0 +1,114 @@
# See https://docs.python.org/3/reference/expressions.html#operator-precedence
# for the official docs on operator precedence.
#
# Most importantly, `and` *always* takes precedence over `or`.
#
# `not` (the third boolean/logical operator) takes precedence over both,
# but the rule there is easier to remember,
# so we don't emit a diagnostic if a `not` expression is unparenthesized
# as part of a chain.
a, b, c = 1, 0, 2
x = a or b and c # RUF021: => `a or (b and c)`
x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
a, b, c = 0, 1, 2
y = a and b or c # RUF021: => `(a and b) or c`
a, b, c, d = 1, 2, 0, 3
if a or b or c and d: # RUF021: => `a or b or (c and d)`
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or b and c or d: # RUF021: => `a or (b and c) or d`
pass
a, b, c, d = 0, 1, 0, 2
while a and b or c and d: # RUF021: => `(and b) or (c and d)`
pass
b, c, d, e = 2, 3, 0, 4
# RUF021: => `a or b or c or (d and e)`:
z = [a for a in range(5) if a or b or c or d and e]
a, b, c, d = 0, 1, 3, 0
assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
if (not a and b) or c or d: # OK
pass
if (
some_reasonably_long_condition
or some_other_reasonably_long_condition
and some_third_reasonably_long_condition
or some_fourth_reasonably_long_condition
and some_fifth_reasonably_long_condition
# a commment
and some_sixth_reasonably_long_condition
and some_seventh_reasonably_long_condition
# another comment
or some_eighth_reasonably_long_condition
):
pass
#############################################
# If they're all the same operator, it's fine
#############################################
x = not a and c # OK
if a or b or c: # OK
pass
while a and b and c: # OK
pass
###########################################################
# We don't consider `not` as part of a chain as problematic
###########################################################
x = not a or not b or not c # OK
#####################################
# If they're parenthesized, it's fine
#####################################
a, b, c = 1, 0, 2
x = a or (b and c) # OK
x2 = (a or b) and c # OK
x3 = (a or b) or c # OK
x4 = (a and b) and c # OK
a, b, c = 0, 1, 2
y = (a and b) or c # OK
yy = a and (b or c) # OK
a, b, c, d = 1, 2, 0, 3
if a or b or (c and d): # OK
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or (b and c) or d: # OK
pass
a, b, c, d = 0, 1, 0, 2
while (a and b) or (c and d): # OK
pass
b, c, d, e = 2, 3, 0, 4
z = [a for a in range(5) if a or b or c or (d and e)] # OK
a, b = 1, 2
if (not a) or b: # OK
if (not a) and b: # OK
pass
a, b, c, d = 0, 1, 3, 0
assert ((not a) and b) or c or d # OK

View File

@@ -1,12 +1,14 @@
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::rules::{
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint,
ruff,
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
@@ -26,6 +28,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UndefinedLocal,
Rule::UnusedAnnotation,
Rule::UnusedClassMethodArgument,
Rule::BuiltinAttributeShadowing,
Rule::UnusedFunctionArgument,
Rule::UnusedImport,
Rule::UnusedLambdaArgument,
@@ -144,20 +147,17 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
));
@@ -236,25 +236,47 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
}
if checker.settings.preview.is_enabled() {
if let Some(import) = binding.as_any_import() {
if let Some(source) = binding.source {
diagnostic.try_set_fix(|| {
let statement = checker.semantic().statement(source);
let parent = checker.semantic().parent_statement(source);
let edit = fix::edits::remove_unused_imports(
std::iter::once(import.member_name().as_ref()),
statement,
parent,
checker.locator(),
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(source),
)))
});
}
}
}
diagnostics.push(diagnostic);
}
}
@@ -277,6 +299,18 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
}
if let Some(class_def) = scope.kind.as_class() {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker,
scope_id,
scope,
class_def,
&mut diagnostics,
);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);

View File

@@ -242,13 +242,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
);
}
} else {
if !checker.semantic.current_scope().kind.is_class() {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range);
}
@@ -724,7 +718,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, call);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@@ -869,6 +863,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DictGetWithNoneDefault) {
flake8_simplify::rules::dict_get_with_none_default(checker, expr);
}
if checker.enabled(Rule::ZipDictKeysAndValues) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
@@ -962,6 +959,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::TrioZeroSleepCall) {
flake8_trio::rules::zero_sleep_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryDunderCall) {
pylint::rules::unnecessary_dunder_call(checker, call);
}
if checker.enabled(Rule::SslWithNoVersion) {
flake8_bandit::rules::ssl_with_no_version(checker, call);
}
if checker.enabled(Rule::SslInsecureVersion) {
flake8_bandit::rules::ssl_insecure_version(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
@@ -1387,12 +1393,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::reimplemented_starmap(checker, &comp.into());
}
}
Expr::DictComp(ast::ExprDictComp {
key,
value,
generators,
range: _,
}) => {
Expr::DictComp(
dict_comp @ ast::ExprDictComp {
key,
value,
generators,
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
@@ -1413,7 +1421,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::StaticKeyDictComprehension) {
ruff::rules::static_key_dict_comprehension(checker, key);
ruff::rules::static_key_dict_comprehension(checker, dict_comp);
}
}
Expr::GeneratorExp(
@@ -1481,6 +1489,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryKeyCheck) {
ruff::rules::unnecessary_key_check(checker, expr);
}
if checker.enabled(Rule::ParenthesizeChainedOperators) {
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
}
}
Expr::NamedExpr(..) => {
if checker.enabled(Rule::AssignmentInAssert) {

View File

@@ -347,17 +347,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
class_def,
name,
decorator_list,
name.range(),
);
}
} else {
if !checker.semantic.current_scope().kind.is_class() {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}
@@ -374,6 +364,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
}
if checker.enabled(Rule::SslWithBadDefaults) {
flake8_bandit::rules::ssl_with_bad_defaults(checker, function_def);
}
}
Stmt::Return(_) => {
if checker.enabled(Rule::ReturnOutsideFunction) {
@@ -552,6 +545,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_import(checker, stmt);
}
if checker.any_enabled(&[
Rule::SuspiciousTelnetlibImport,
Rule::SuspiciousFtplibImport,
Rule::SuspiciousPickleImport,
Rule::SuspiciousSubprocessImport,
Rule::SuspiciousXmlEtreeImport,
Rule::SuspiciousXmlSaxImport,
Rule::SuspiciousXmlExpatImport,
Rule::SuspiciousXmlMinidomImport,
Rule::SuspiciousXmlPulldomImport,
Rule::SuspiciousLxmlImport,
Rule::SuspiciousXmlrpcImport,
Rule::SuspiciousHttpoxyImport,
Rule::SuspiciousPycryptoImport,
Rule::SuspiciousPyghmiImport,
]) {
flake8_bandit::rules::suspicious_imports(checker, stmt);
}
for alias in names {
if checker.enabled(Rule::NonAsciiImportName) {
@@ -751,6 +762,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::unnecessary_builtin_import(checker, stmt, module, names);
}
}
if checker.any_enabled(&[
Rule::SuspiciousTelnetlibImport,
Rule::SuspiciousFtplibImport,
Rule::SuspiciousPickleImport,
Rule::SuspiciousSubprocessImport,
Rule::SuspiciousXmlEtreeImport,
Rule::SuspiciousXmlSaxImport,
Rule::SuspiciousXmlExpatImport,
Rule::SuspiciousXmlMinidomImport,
Rule::SuspiciousXmlPulldomImport,
Rule::SuspiciousLxmlImport,
Rule::SuspiciousXmlrpcImport,
Rule::SuspiciousHttpoxyImport,
Rule::SuspiciousPycryptoImport,
Rule::SuspiciousPyghmiImport,
]) {
flake8_bandit::rules::suspicious_imports(checker, stmt);
}
if checker.enabled(Rule::BannedApi) {
if let Some(module) =
helpers::resolve_imported_module_path(level, module, checker.module_path)

View File

@@ -37,7 +37,7 @@ use ruff_python_ast::{
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_notebook::CellOffsets;
use ruff_notebook::{CellOffsets, NotebookIndex};
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
use ruff_python_ast::helpers::{
collect_import_from_member, extract_handled_exceptions, to_module_path,
@@ -56,7 +56,7 @@ use ruff_python_semantic::{
StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
use ruff_source_file::Locator;
use ruff_source_file::{Locator, OneIndexed, SourceRow};
use crate::checkers::ast::annotation::AnnotationContext;
use crate::checkers::ast::deferred::Deferred;
@@ -83,6 +83,8 @@ pub(crate) struct Checker<'a> {
pub(crate) source_type: PySourceType,
/// The [`CellOffsets`] for the current file, if it's a Jupyter notebook.
cell_offsets: Option<&'a CellOffsets>,
/// The [`NotebookIndex`] for the current file, if it's a Jupyter notebook.
notebook_index: Option<&'a NotebookIndex>,
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
/// comments).
noqa: flags::Noqa,
@@ -128,6 +130,7 @@ impl<'a> Checker<'a> {
importer: Importer<'a>,
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>,
) -> Checker<'a> {
Checker {
settings,
@@ -146,6 +149,7 @@ impl<'a> Checker<'a> {
diagnostics: Vec::default(),
flake8_bugbear_seen: Vec::default(),
cell_offsets,
notebook_index,
last_stmt_end: TextSize::default(),
}
}
@@ -198,6 +202,20 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`SourceRow`] for the given offset.
pub(crate) fn compute_source_row(&self, offset: TextSize) -> SourceRow {
#[allow(deprecated)]
let line = self.locator.compute_line_index(offset);
if let Some(notebook_index) = self.notebook_index {
let cell = notebook_index.cell(line).unwrap_or(OneIndexed::MIN);
let line = notebook_index.cell_row(line).unwrap_or(OneIndexed::MIN);
SourceRow::Notebook { cell, line }
} else {
SourceRow::SourceFile { line }
}
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -1984,6 +2002,7 @@ pub(crate) fn check_ast(
package: Option<&Path>,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
notebook_index: Option<&NotebookIndex>,
) -> Vec<Diagnostic> {
let module_path = package.and_then(|package| to_module_path(package, path));
let module = Module {
@@ -2013,6 +2032,7 @@ pub(crate) fn check_ast(
Importer::new(python_ast, locator, stylist),
source_type,
cell_offsets,
notebook_index,
);
checker.bind_builtins();

View File

@@ -214,6 +214,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
(Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall),
#[allow(deprecated)]
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
@@ -471,6 +472,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
#[allow(deprecated)]
@@ -626,7 +628,24 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "321") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousFTPLibUsage),
(Flake8Bandit, "323") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage),
(Flake8Bandit, "324") => (RuleGroup::Stable, rules::flake8_bandit::rules::HashlibInsecureHashFunction),
(Flake8Bandit, "401") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousTelnetlibImport),
(Flake8Bandit, "402") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousFtplibImport),
(Flake8Bandit, "403") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPickleImport),
(Flake8Bandit, "404") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousSubprocessImport),
(Flake8Bandit, "405") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlEtreeImport),
(Flake8Bandit, "406") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlSaxImport),
(Flake8Bandit, "407") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlExpatImport),
(Flake8Bandit, "408") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlMinidomImport),
(Flake8Bandit, "409") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlPulldomImport),
(Flake8Bandit, "410") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousLxmlImport),
(Flake8Bandit, "411") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlrpcImport),
(Flake8Bandit, "412") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousHttpoxyImport),
(Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
(Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport),
(Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation),
(Flake8Bandit, "502") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslInsecureVersion),
(Flake8Bandit, "503") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithBadDefaults),
(Flake8Bandit, "504") => (RuleGroup::Preview, rules::flake8_bandit::rules::SslWithNoVersion),
(Flake8Bandit, "505") => (RuleGroup::Preview, rules::flake8_bandit::rules::WeakCryptographicKey),
(Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad),
(Flake8Bandit, "507") => (RuleGroup::Preview, rules::flake8_bandit::rules::SSHNoHostKeyVerification),
@@ -905,6 +924,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -79,7 +79,7 @@ pub fn extract_directives(
NoqaMapping::default()
},
isort: if flags.intersects(Flags::ISORT) {
extract_isort_directives(lxr, locator)
extract_isort_directives(locator, indexer)
} else {
IsortDirectives::default()
},
@@ -215,15 +215,13 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
}
/// Extract a set of ranges over which to disable isort.
fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
fn extract_isort_directives(locator: &Locator, indexer: &Indexer) -> IsortDirectives {
let mut exclusions: Vec<TextRange> = Vec::default();
let mut splits: Vec<TextSize> = Vec::default();
let mut off: Option<TextSize> = None;
for &(ref tok, range) in lxr.iter().flatten() {
let Tok::Comment(comment_text) = tok else {
continue;
};
for range in indexer.comment_ranges() {
let comment_text = locator.slice(range);
// `isort` allows for `# isort: skip` and `# isort: skip_file` to include or
// omit a space after the colon. The remaining action comments are
@@ -592,8 +590,10 @@ assert foo, \
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::default()
);
@@ -603,8 +603,10 @@ y = 2
# isort: on
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(25))])
);
@@ -616,8 +618,10 @@ y = 2
z = x + 1
# isort: on";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(38))])
);
@@ -626,8 +630,10 @@ x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::from_iter([TextRange::at(TextSize::from(0), contents.text_len())])
);
@@ -636,8 +642,10 @@ x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::default()
);
@@ -648,8 +656,10 @@ y = 2
# isort: skip_file
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
extract_isort_directives(&locator, &indexer).exclusions,
Vec::default()
);
}
@@ -660,8 +670,10 @@ z = x + 1";
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
extract_isort_directives(&locator, &indexer).splits,
Vec::new()
);
@@ -670,8 +682,10 @@ y = 2
# isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
extract_isort_directives(&locator, &indexer).splits,
vec![TextSize::from(12)]
);
@@ -679,8 +693,10 @@ z = x + 1";
y = 2 # isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
extract_isort_directives(&locator, &indexer).splits,
vec![TextSize::from(13)]
);
}

View File

@@ -153,13 +153,17 @@ impl<'a> SectionContexts<'a> {
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
let indent_size = indent.text_len();
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
let section_name = leading_words(&line);
let section_name_size = section_name.text_len();
if is_docstring_section(
&line,
section_name_range,
indent_size,
section_name_size,
section_kind,
last.as_ref(),
previous_line.as_ref(),
lines.peek(),
) {
@@ -170,7 +174,8 @@ impl<'a> SectionContexts<'a> {
last = Some(SectionContextData {
kind: section_kind,
name_range: section_name_range + line.start(),
indent_size: indent.text_len(),
name_range: TextRange::at(line.start() + indent_size, section_name_size),
range: TextRange::empty(line.start()),
summary_full_end: line.full_end(),
});
@@ -204,8 +209,8 @@ impl<'a> SectionContexts<'a> {
}
impl<'a> IntoIterator for &'a SectionContexts<'a> {
type IntoIter = SectionContextsIter<'a>;
type Item = SectionContext<'a>;
type IntoIter = SectionContextsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
@@ -257,6 +262,9 @@ impl ExactSizeIterator for SectionContextsIter<'_> {}
struct SectionContextData {
kind: SectionKind,
/// The size of the indentation of the section name.
indent_size: TextSize,
/// Range of the section name, relative to the [`Docstring::body`]
name_range: TextRange,
@@ -401,12 +409,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
/// Check if the suspected context is really a section header.
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
indent_size: TextSize,
section_name_size: TextSize,
section_kind: SectionKind,
previous_section: Option<&SectionContextData>,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
@@ -439,5 +450,25 @@ fn is_docstring_section(
return false;
}
// Determine if this is a sub-section within another section, like `args` in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
if section_kind.as_str() != verbatim {
return false;
}
}
}
true
}

View File

@@ -148,6 +148,7 @@ pub fn check_path(
match tokens.into_ast_source(source_kind, source_type) {
Ok(python_ast) => {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
&python_ast,
@@ -161,6 +162,7 @@ pub fn check_path(
package,
source_type,
cell_offsets,
notebook_index,
));
}
if use_imports {

View File

@@ -216,12 +216,7 @@ impl Display for DisplayParseError {
colon = ":".cyan(),
)?;
} else {
write!(
f,
"{header}{colon}",
header = "Failed to parse".bold(),
colon = ":".cyan(),
)?;
write!(f, "{header}", header = "Failed to parse at ".bold())?;
}
match &self.location {
ErrorLocation::File(location) => {

View File

@@ -3,7 +3,7 @@ use ruff_macros::CacheKey;
use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
const RULESET_SIZE: usize = 12;
const RULESET_SIZE: usize = 13;
/// A set of [`Rule`]s.
///

View File

@@ -89,7 +89,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
// if x > 0:
// return 1
// ```
if terminal == Terminal::ConditionalReturn || terminal == Terminal::None {
if terminal.has_implicit_return() {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}

View File

@@ -263,14 +263,14 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `str | int`
= help: Add return type annotation: `str | int | None`
Unsafe fix
79 79 | return 1
80 80 |
81 81 |
82 |-def func(x: int):
82 |+def func(x: int) -> str | int:
82 |+def func(x: int) -> str | int | None:
83 83 | match x:
84 84 | case [1, 2, 3]:
85 85 | return 1
@@ -396,14 +396,14 @@ auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
= help: Add return type annotation: `int | None`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
137 |+def func(x: int) -> int | None:
138 138 | try:
139 139 | return 1
140 140 | except:
@@ -674,4 +674,99 @@ auto_return_type.py:262:5: ANN201 [*] Missing return type annotation for public
264 264 | if x > 0:
265 265 | return 1
auto_return_type.py:269:5: ANN201 [*] Missing return type annotation for public function `func`
|
269 | def func(x: int):
| ^^^^ ANN201
270 | if x > 5:
271 | raise ValueError
|
= help: Add return type annotation: `None`
Unsafe fix
266 266 | raise ValueError
267 267 |
268 268 |
269 |-def func(x: int):
269 |+def func(x: int) -> None:
270 270 | if x > 5:
271 271 | raise ValueError
272 272 | else:
auto_return_type.py:276:5: ANN201 [*] Missing return type annotation for public function `func`
|
276 | def func(x: int):
| ^^^^ ANN201
277 | if x > 5:
278 | raise ValueError
|
= help: Add return type annotation: `None`
Unsafe fix
273 273 | pass
274 274 |
275 275 |
276 |-def func(x: int):
276 |+def func(x: int) -> None:
277 277 | if x > 5:
278 278 | raise ValueError
279 279 | elif x > 10:
auto_return_type.py:283:5: ANN201 [*] Missing return type annotation for public function `func`
|
283 | def func(x: int):
| ^^^^ ANN201
284 | if x > 5:
285 | raise ValueError
|
= help: Add return type annotation: `int | None`
Unsafe fix
280 280 | pass
281 281 |
282 282 |
283 |-def func(x: int):
283 |+def func(x: int) -> int | None:
284 284 | if x > 5:
285 285 | raise ValueError
286 286 | elif x > 10:
auto_return_type.py:290:5: ANN201 [*] Missing return type annotation for public function `func`
|
290 | def func():
| ^^^^ ANN201
291 | try:
292 | return 5
|
= help: Add return type annotation: `int`
Unsafe fix
287 287 | return 5
288 288 |
289 289 |
290 |-def func():
290 |+def func() -> int:
291 291 | try:
292 292 | return 5
293 293 | except:
auto_return_type.py:299:5: ANN201 [*] Missing return type annotation for public function `func`
|
299 | def func(x: int):
| ^^^^ ANN201
300 | match x:
301 | case [1, 2, 3]:
|
= help: Add return type annotation: `str | int`
Unsafe fix
296 296 | raise ValueError
297 297 |
298 298 |
299 |-def func(x: int):
299 |+def func(x: int) -> str | int:
300 300 | match x:
301 301 | case [1, 2, 3]:
302 302 | return 1

View File

@@ -293,7 +293,7 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `Union[str | int]`
= help: Add return type annotation: `Union[str | int | None]`
Unsafe fix
1 |+from typing import Union
@@ -305,7 +305,7 @@ auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public f
80 81 |
81 82 |
82 |-def func(x: int):
83 |+def func(x: int) -> Union[str | int]:
83 |+def func(x: int) -> Union[str | int | None]:
83 84 | match x:
84 85 | case [1, 2, 3]:
85 86 | return 1
@@ -446,17 +446,22 @@ auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
= help: Add return type annotation: `Optional[int]`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
134 135 | return 2
135 136 |
136 137 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
138 |+def func(x: int) -> Optional[int]:
138 139 | try:
139 140 | return 1
140 141 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
@@ -755,4 +760,117 @@ auto_return_type.py:262:5: ANN201 [*] Missing return type annotation for public
264 264 | if x > 0:
265 265 | return 1
auto_return_type.py:269:5: ANN201 [*] Missing return type annotation for public function `func`
|
269 | def func(x: int):
| ^^^^ ANN201
270 | if x > 5:
271 | raise ValueError
|
= help: Add return type annotation: `None`
Unsafe fix
266 266 | raise ValueError
267 267 |
268 268 |
269 |-def func(x: int):
269 |+def func(x: int) -> None:
270 270 | if x > 5:
271 271 | raise ValueError
272 272 | else:
auto_return_type.py:276:5: ANN201 [*] Missing return type annotation for public function `func`
|
276 | def func(x: int):
| ^^^^ ANN201
277 | if x > 5:
278 | raise ValueError
|
= help: Add return type annotation: `None`
Unsafe fix
273 273 | pass
274 274 |
275 275 |
276 |-def func(x: int):
276 |+def func(x: int) -> None:
277 277 | if x > 5:
278 278 | raise ValueError
279 279 | elif x > 10:
auto_return_type.py:283:5: ANN201 [*] Missing return type annotation for public function `func`
|
283 | def func(x: int):
| ^^^^ ANN201
284 | if x > 5:
285 | raise ValueError
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
214 214 | return 1
215 215 |
216 216 |
217 |-from typing import overload
217 |+from typing import overload, Optional
218 218 |
219 219 |
220 220 | @overload
--------------------------------------------------------------------------------
280 280 | pass
281 281 |
282 282 |
283 |-def func(x: int):
283 |+def func(x: int) -> Optional[int]:
284 284 | if x > 5:
285 285 | raise ValueError
286 286 | elif x > 10:
auto_return_type.py:290:5: ANN201 [*] Missing return type annotation for public function `func`
|
290 | def func():
| ^^^^ ANN201
291 | try:
292 | return 5
|
= help: Add return type annotation: `int`
Unsafe fix
287 287 | return 5
288 288 |
289 289 |
290 |-def func():
290 |+def func() -> int:
291 291 | try:
292 292 | return 5
293 293 | except:
auto_return_type.py:299:5: ANN201 [*] Missing return type annotation for public function `func`
|
299 | def func(x: int):
| ^^^^ ANN201
300 | match x:
301 | case [1, 2, 3]:
|
= help: Add return type annotation: `Union[str | int]`
Unsafe fix
214 214 | return 1
215 215 |
216 216 |
217 |-from typing import overload
217 |+from typing import overload, Union
218 218 |
219 219 |
220 220 | @overload
--------------------------------------------------------------------------------
296 296 | raise ValueError
297 297 |
298 298 |
299 |-def func(x: int):
299 |+def func(x: int) -> Union[str | int]:
300 300 | match x:
301 301 | case [1, 2, 3]:
302 302 | return 1

View File

@@ -36,6 +36,9 @@ mod tests {
#[test_case(Rule::SSHNoHostKeyVerification, Path::new("S507.py"))]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
#[test_case(Rule::SslInsecureVersion, Path::new("S502.py"))]
#[test_case(Rule::SslWithBadDefaults, Path::new("S503.py"))]
#[test_case(Rule::SslWithNoVersion, Path::new("S504.py"))]
#[test_case(Rule::StartProcessWithAShell, Path::new("S605.py"))]
#[test_case(Rule::StartProcessWithNoShell, Path::new("S606.py"))]
#[test_case(Rule::StartProcessWithPartialPath, Path::new("S607.py"))]
@@ -45,6 +48,20 @@ mod tests {
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
#[test_case(Rule::SuspiciousFtplibImport, Path::new("S402.py"))]
#[test_case(Rule::SuspiciousPickleImport, Path::new("S403.py"))]
#[test_case(Rule::SuspiciousSubprocessImport, Path::new("S404.py"))]
#[test_case(Rule::SuspiciousXmlEtreeImport, Path::new("S405.py"))]
#[test_case(Rule::SuspiciousXmlSaxImport, Path::new("S406.py"))]
#[test_case(Rule::SuspiciousXmlExpatImport, Path::new("S407.py"))]
#[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.py"))]
#[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.py"))]
#[test_case(Rule::SuspiciousLxmlImport, Path::new("S410.py"))]
#[test_case(Rule::SuspiciousXmlrpcImport, Path::new("S411.py"))]
#[test_case(Rule::SuspiciousHttpoxyImport, Path::new("S412.py"))]
#[test_case(Rule::SuspiciousPycryptoImport, Path::new("S413.py"))]
#[test_case(Rule::SuspiciousPyghmiImport, Path::new("S415.py"))]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]

View File

@@ -20,7 +20,11 @@ pub(crate) use shell_injection::*;
pub(crate) use snmp_insecure_version::*;
pub(crate) use snmp_weak_cryptography::*;
pub(crate) use ssh_no_host_key_verification::*;
pub(crate) use ssl_insecure_version::*;
pub(crate) use ssl_with_bad_defaults::*;
pub(crate) use ssl_with_no_version::*;
pub(crate) use suspicious_function_call::*;
pub(crate) use suspicious_imports::*;
pub(crate) use tarfile_unsafe_members::*;
pub(crate) use try_except_continue::*;
pub(crate) use try_except_pass::*;
@@ -49,7 +53,11 @@ mod shell_injection;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod ssh_no_host_key_verification;
mod ssl_insecure_version;
mod ssl_with_bad_defaults;
mod ssl_with_no_version;
mod suspicious_function_call;
mod suspicious_imports;
mod tarfile_unsafe_members;
mod try_except_continue;
mod try_except_pass;

View File

@@ -0,0 +1,107 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function calls with parameters that indicate the use of insecure
/// SSL and TLS protocol versions.
///
/// ## Why is this bad?
/// Several highly publicized exploitable flaws have been discovered in all
/// versions of SSL and early versions of TLS. The following versions are
/// considered insecure, and should be avoided:
/// - SSL v2
/// - SSL v3
/// - TLS v1
/// - TLS v1.1
///
/// This method supports detection on the Python's built-in `ssl` module and
/// the `pyOpenSSL` module.
///
/// ## Example
/// ```python
/// import ssl
///
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1)
/// ```
///
/// Use instead:
/// ```python
/// import ssl
///
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2)
/// ```
#[violation]
pub struct SslInsecureVersion {
protocol: String,
}
impl Violation for SslInsecureVersion {
#[derive_message_formats]
fn message(&self) -> String {
let SslInsecureVersion { protocol } = self;
format!("Call made with insecure SSL protocol: `{protocol}`")
}
}
/// S502
pub(crate) fn ssl_insecure_version(checker: &mut Checker, call: &ExprCall) {
let Some(keyword) = checker
.semantic()
.resolve_call_path(call.func.as_ref())
.and_then(|call_path| match call_path.as_slice() {
["ssl", "wrap_socket"] => Some("ssl_version"),
["OpenSSL", "SSL", "Context"] => Some("method"),
_ => None,
})
else {
return;
};
let Some(keyword) = call.arguments.find_keyword(keyword) else {
return;
};
match &keyword.value {
Expr::Name(ast::ExprName { id, .. }) => {
if is_insecure_protocol(id) {
checker.diagnostics.push(Diagnostic::new(
SslInsecureVersion {
protocol: id.to_string(),
},
keyword.range(),
));
}
}
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
if is_insecure_protocol(attr) {
checker.diagnostics.push(Diagnostic::new(
SslInsecureVersion {
protocol: attr.to_string(),
},
keyword.range(),
));
}
}
_ => {}
}
}
/// Returns `true` if the given protocol name is insecure.
fn is_insecure_protocol(name: &str) -> bool {
matches!(
name,
"PROTOCOL_SSLv2"
| "PROTOCOL_SSLv3"
| "PROTOCOL_TLSv1"
| "PROTOCOL_TLSv1_1"
| "SSLv2_METHOD"
| "SSLv23_METHOD"
| "SSLv3_METHOD"
| "TLSv1_METHOD"
| "TLSv1_1_METHOD"
)
}

View File

@@ -0,0 +1,106 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, StmtFunctionDef};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function definitions with default arguments set to insecure SSL
/// and TLS protocol versions.
///
/// ## Why is this bad?
/// Several highly publicized exploitable flaws have been discovered in all
/// versions of SSL and early versions of TLS. The following versions are
/// considered insecure, and should be avoided:
/// - SSL v2
/// - SSL v3
/// - TLS v1
/// - TLS v1.1
///
/// ## Example
/// ```python
/// import ssl
///
///
/// def func(version=ssl.PROTOCOL_TLSv1):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// import ssl
///
///
/// def func(version=ssl.PROTOCOL_TLSv1_2):
/// ...
/// ```
#[violation]
pub struct SslWithBadDefaults {
protocol: String,
}
impl Violation for SslWithBadDefaults {
#[derive_message_formats]
fn message(&self) -> String {
let SslWithBadDefaults { protocol } = self;
format!("Argument default set to insecure SSL protocol: `{protocol}`")
}
}
/// S503
pub(crate) fn ssl_with_bad_defaults(checker: &mut Checker, function_def: &StmtFunctionDef) {
function_def
.parameters
.posonlyargs
.iter()
.chain(
function_def
.parameters
.args
.iter()
.chain(function_def.parameters.kwonlyargs.iter()),
)
.for_each(|param| {
if let Some(default) = &param.default {
match default.as_ref() {
Expr::Name(ast::ExprName { id, range, .. }) => {
if is_insecure_protocol(id.as_str()) {
checker.diagnostics.push(Diagnostic::new(
SslWithBadDefaults {
protocol: id.to_string(),
},
*range,
));
}
}
Expr::Attribute(ast::ExprAttribute { attr, range, .. }) => {
if is_insecure_protocol(attr.as_str()) {
checker.diagnostics.push(Diagnostic::new(
SslWithBadDefaults {
protocol: attr.to_string(),
},
*range,
));
}
}
_ => {}
}
}
});
}
/// Returns `true` if the given protocol name is insecure.
fn is_insecure_protocol(name: &str) -> bool {
matches!(
name,
"PROTOCOL_SSLv2"
| "PROTOCOL_SSLv3"
| "PROTOCOL_TLSv1"
| "PROTOCOL_TLSv1_1"
| "SSLv2_METHOD"
| "SSLv23_METHOD"
| "SSLv3_METHOD"
| "TLSv1_METHOD"
| "TLSv1_1_METHOD"
)
}

View File

@@ -0,0 +1,51 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for calls to `ssl.wrap_socket()` without an `ssl_version`.
///
/// ## Why is this bad?
/// This method is known to provide a default value that maximizes
/// compatibility, but permits use of insecure protocols.
///
/// ## Example
/// ```python
/// import ssl
///
/// ssl.wrap_socket()
/// ```
///
/// Use instead:
/// ```python
/// import ssl
///
/// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2)
/// ```
#[violation]
pub struct SslWithNoVersion;
impl Violation for SslWithNoVersion {
#[derive_message_formats]
fn message(&self) -> String {
format!("`ssl.wrap_socket` called without an `ssl_version``")
}
}
/// S504
pub(crate) fn ssl_with_no_version(checker: &mut Checker, call: &ExprCall) {
if checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ssl", "wrap_socket"]))
{
if call.arguments.find_keyword("ssl_version").is_none() {
checker
.diagnostics
.push(Diagnostic::new(SslWithNoVersion, call.range()));
}
}
}

View File

@@ -0,0 +1,592 @@
//! Check for imports of or from suspicious modules.
//!
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html>
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Stmt};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for imports of the`telnetlib` module.
///
/// ## Why is this bad?
/// Telnet is considered insecure. Instead, use SSH or another encrypted
/// protocol.
///
/// ## Example
/// ```python
/// import telnetlib
/// ```
#[violation]
pub struct SuspiciousTelnetlibImport;
impl Violation for SuspiciousTelnetlibImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.")
}
}
/// ## What it does
/// Checks for imports of the `ftplib` module.
///
/// ## Why is this bad?
/// FTP is considered insecure. Instead, use SSH, SFTP, SCP, or another
/// encrypted protocol.
///
/// ## Example
/// ```python
/// import ftplib
/// ```
#[violation]
pub struct SuspiciousFtplibImport;
impl Violation for SuspiciousFtplibImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.")
}
}
/// ## What it does
/// Checks for imports of the `pickle`, `cPickle`, `dill`, and `shelve` modules.
///
/// ## Why is this bad?
/// It is possible to construct malicious pickle data which will execute
/// arbitrary code during unpickling. Consider possible security implications
/// associated with these modules.
///
/// ## Example
/// ```python
/// import pickle
/// ```
/// /// ## References
/// - [Python Docs](https://docs.python.org/3/library/pickle.html)
#[violation]
pub struct SuspiciousPickleImport;
impl Violation for SuspiciousPickleImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure")
}
}
/// ## What it does
/// Checks for imports of the `subprocess` module.
///
/// ## Why is this bad?
/// It is possible to inject malicious commands into subprocess calls. Consider
/// possible security implications associated with this module.
///
/// ## Example
/// ```python
/// import subprocess
/// ```
#[violation]
pub struct SuspiciousSubprocessImport;
impl Violation for SuspiciousSubprocessImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`subprocess` module is possibly insecure")
}
}
/// ## What it does
/// Checks for imports of the `xml.etree.cElementTree` and `xml.etree.ElementTree` modules
///
/// ## Why is this bad?
/// Using various methods from these modules to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
/// called before parsing XML data.
///
/// ## Example
/// ```python
/// import xml.etree.cElementTree
/// ```
#[violation]
pub struct SuspiciousXmlEtreeImport;
impl Violation for SuspiciousXmlEtreeImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`xml.etree` methods are vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the `xml.sax` module.
///
/// ## Why is this bad?
/// Using various methods from these modules to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
/// called before parsing XML data.
///
/// ## Example
/// ```python
/// import xml.sax
/// ```
#[violation]
pub struct SuspiciousXmlSaxImport;
impl Violation for SuspiciousXmlSaxImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`xml.sax` methods are vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the `xml.dom.expatbuilder` module.
///
/// ## Why is this bad?
/// Using various methods from these modules to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
/// called before parsing XML data.
///
/// ## Example
/// ```python
/// import xml.dom.expatbuilder
/// ```
#[violation]
pub struct SuspiciousXmlExpatImport;
impl Violation for SuspiciousXmlExpatImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`xml.dom.expatbuilder` is vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the `xml.dom.minidom` module.
///
/// ## Why is this bad?
/// Using various methods from these modules to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
/// called before parsing XML data.
///
/// ## Example
/// ```python
/// import xml.dom.minidom
/// ```
#[violation]
pub struct SuspiciousXmlMinidomImport;
impl Violation for SuspiciousXmlMinidomImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`xml.dom.minidom` is vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the `xml.dom.pulldom` module.
///
/// ## Why is this bad?
/// Using various methods from these modules to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package, or make sure `defusedxml.defuse_stdlib()` is
/// called before parsing XML data.
///
/// ## Example
/// ```python
/// import xml.dom.pulldom
/// ```
#[violation]
pub struct SuspiciousXmlPulldomImport;
impl Violation for SuspiciousXmlPulldomImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`xml.dom.pulldom` is vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the`lxml` module.
///
/// ## Why is this bad?
/// Using various methods from the `lxml` module to parse untrusted XML data is
/// known to be vulnerable to XML attacks. Replace vulnerable imports with the
/// equivalent `defusedxml` package.
///
/// ## Example
/// ```python
/// import lxml
/// ```
#[violation]
pub struct SuspiciousLxmlImport;
impl Violation for SuspiciousLxmlImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`lxml` is vulnerable to XML attacks")
}
}
/// ## What it does
/// Checks for imports of the `xmlrpc` module.
///
/// ## Why is this bad?
/// XMLRPC is a particularly dangerous XML module as it is also concerned with
/// communicating data over a network. Use the `defused.xmlrpc.monkey_patch()`
/// function to monkey-patch the `xmlrpclib` module and mitigate remote XML
/// attacks.
///
/// ## Example
/// ```python
/// import xmlrpc
/// ```
#[violation]
pub struct SuspiciousXmlrpcImport;
impl Violation for SuspiciousXmlrpcImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("XMLRPC is vulnerable to remote XML attacks")
}
}
/// ## What it does
/// Checks for imports of `wsgiref.handlers.CGIHandler` and
/// `twisted.web.twcgi.CGIScript`.
///
/// ## Why is this bad?
/// httpoxy is a set of vulnerabilities that affect application code running in
/// CGI or CGI-like environments. The use of CGI for web applications should be
/// avoided to prevent this class of attack.
///
/// ## Example
/// ```python
/// import wsgiref.handlers.CGIHandler
/// ```
///
/// ## References
/// - [httpoxy website](https://httpoxy.org/)
#[violation]
pub struct SuspiciousHttpoxyImport;
impl Violation for SuspiciousHttpoxyImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("`httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided")
}
}
/// ## What it does
/// Checks for imports of several unsafe cryptography modules.
///
/// ## Why is this bad?
/// The `pycrypto` library is known to have a publicly disclosed buffer
/// overflow vulnerability. It is no longer actively maintained and has been
/// deprecated in favor of the `pyca/cryptography` library.
///
/// ## Example
/// ```python
/// import Crypto.Random
/// ```
///
/// ## References
/// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176)
#[violation]
pub struct SuspiciousPycryptoImport;
impl Violation for SuspiciousPycryptoImport {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"`pycrypto` library is known to have publicly disclosed buffer overflow vulnerability"
)
}
}
/// ## What it does
/// Checks for imports of the `pyghmi` module.
///
/// ## Why is this bad?
/// `pyghmi` is an IPMI-related module, but IPMI is considered insecure.
/// Instead, use an encrypted protocol.
///
/// ## Example
/// ```python
/// import pyghmi
/// ```
///
/// ## References
/// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176)
#[violation]
pub struct SuspiciousPyghmiImport;
impl Violation for SuspiciousPyghmiImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.")
}
}
/// S401, S402, S403, S404, S405, S406, S407, S408, S409, S410, S411, S412, S413, S415
pub(crate) fn suspicious_imports(checker: &mut Checker, stmt: &Stmt) {
match stmt {
Stmt::Import(ast::StmtImport { names, .. }) => {
for name in names {
match name.name.as_str() {
"telnetlib" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousTelnetlibImport),
name.range,
),
"ftplib" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousFtplibImport),
name.range,
),
"pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPickleImport),
name.range,
),
"subprocess" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousSubprocessImport),
name.range,
),
"xml.etree.cElementTree" | "xml.etree.ElementTree" => {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlEtreeImport),
name.range,
);
}
"xml.sax" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlSaxImport),
name.range,
),
"xml.dom.expatbuilder" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlExpatImport),
name.range,
),
"xml.dom.minidom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlMinidomImport),
name.range,
),
"xml.dom.pulldom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlPulldomImport),
name.range,
),
"lxml" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousLxmlImport),
name.range,
),
"xmlrpc" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlrpcImport),
name.range,
),
"Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol"
| "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPycryptoImport),
name.range,
);
}
"pyghmi" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPyghmiImport),
name.range,
),
_ => {}
}
}
}
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
let Some(identifier) = module else { return };
match identifier.as_str() {
"telnetlib" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousTelnetlibImport),
identifier.range(),
),
"ftplib" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousFtplibImport),
identifier.range(),
),
"pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPickleImport),
identifier.range(),
),
"subprocess" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousSubprocessImport),
identifier.range(),
),
"xml.etree" => {
for name in names {
if matches!(name.name.as_str(), "cElementTree" | "ElementTree") {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlEtreeImport),
identifier.range(),
);
}
}
}
"xml.etree.cElementTree" | "xml.etree.ElementTree" => {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlEtreeImport),
identifier.range(),
);
}
"xml" => {
for name in names {
if name.name.as_str() == "sax" {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlSaxImport),
identifier.range(),
);
}
}
}
"xml.sax" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlSaxImport),
identifier.range(),
),
"xml.dom" => {
for name in names {
match name.name.as_str() {
"expatbuilder" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlExpatImport),
identifier.range(),
),
"minidom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlMinidomImport),
identifier.range(),
),
"pulldom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlPulldomImport),
identifier.range(),
),
_ => (),
}
}
}
"xml.dom.expatbuilder" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlExpatImport),
identifier.range(),
),
"xml.dom.minidom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlMinidomImport),
identifier.range(),
),
"xml.dom.pulldom" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlPulldomImport),
identifier.range(),
),
"lxml" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousLxmlImport),
identifier.range(),
),
"xmlrpc" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousXmlrpcImport),
identifier.range(),
),
"wsgiref.handlers" => {
for name in names {
if name.name.as_str() == "CGIHandler" {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousHttpoxyImport),
identifier.range(),
);
}
}
}
"twisted.web.twcgi" => {
for name in names {
if name.name.as_str() == "CGIScript" {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousHttpoxyImport),
identifier.range(),
);
}
}
}
"Crypto" => {
for name in names {
if matches!(
name.name.as_str(),
"Cipher"
| "Hash"
| "IO"
| "Protocol"
| "PublicKey"
| "Random"
| "Signature"
| "Util"
) {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPycryptoImport),
identifier.range(),
);
}
}
}
"Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol"
| "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => {
check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPycryptoImport),
identifier.range(),
);
}
"pyghmi" => check_and_push_diagnostic(
checker,
DiagnosticKind::from(SuspiciousPyghmiImport),
identifier.range(),
),
_ => {}
}
}
_ => panic!("Expected Stmt::Import | Stmt::ImportFrom"),
};
}
fn check_and_push_diagnostic(
checker: &mut Checker,
diagnostic_kind: DiagnosticKind,
range: TextRange,
) {
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, range);
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S401.py:1:8: S401 `telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.
|
1 | import telnetlib # S401
| ^^^^^^^^^ S401
2 | from telnetlib import Telnet # S401
|
S401.py:2:6: S401 `telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.
|
1 | import telnetlib # S401
2 | from telnetlib import Telnet # S401
| ^^^^^^^^^ S401
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S402.py:1:8: S402 `ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.
|
1 | import ftplib # S402
| ^^^^^^ S402
2 | from ftplib import FTP # S402
|
S402.py:2:6: S402 `ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.
|
1 | import ftplib # S402
2 | from ftplib import FTP # S402
| ^^^^^^ S402
|

View File

@@ -0,0 +1,78 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S403.py:1:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
1 | import dill # S403
| ^^^^ S403
2 | from dill import objects # S403
3 | import shelve
|
S403.py:2:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
1 | import dill # S403
2 | from dill import objects # S403
| ^^^^ S403
3 | import shelve
4 | from shelve import open
|
S403.py:3:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
1 | import dill # S403
2 | from dill import objects # S403
3 | import shelve
| ^^^^^^ S403
4 | from shelve import open
5 | import cPickle
|
S403.py:4:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
2 | from dill import objects # S403
3 | import shelve
4 | from shelve import open
| ^^^^^^ S403
5 | import cPickle
6 | from cPickle import load
|
S403.py:5:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
3 | import shelve
4 | from shelve import open
5 | import cPickle
| ^^^^^^^ S403
6 | from cPickle import load
7 | import pickle
|
S403.py:6:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
4 | from shelve import open
5 | import cPickle
6 | from cPickle import load
| ^^^^^^^ S403
7 | import pickle
8 | from pickle import load
|
S403.py:7:8: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
5 | import cPickle
6 | from cPickle import load
7 | import pickle
| ^^^^^^ S403
8 | from pickle import load
|
S403.py:8:6: S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
6 | from cPickle import load
7 | import pickle
8 | from pickle import load
| ^^^^^^ S403
|

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S404.py:1:8: S404 `subprocess` module is possibly insecure
|
1 | import subprocess # S404
| ^^^^^^^^^^ S404
2 | from subprocess import Popen # S404
3 | from subprocess import Popen as pop # S404
|
S404.py:2:6: S404 `subprocess` module is possibly insecure
|
1 | import subprocess # S404
2 | from subprocess import Popen # S404
| ^^^^^^^^^^ S404
3 | from subprocess import Popen as pop # S404
|
S404.py:3:6: S404 `subprocess` module is possibly insecure
|
1 | import subprocess # S404
2 | from subprocess import Popen # S404
3 | from subprocess import Popen as pop # S404
| ^^^^^^^^^^ S404
|

View File

@@ -0,0 +1,38 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S405.py:1:8: S405 `xml.etree` methods are vulnerable to XML attacks
|
1 | import xml.etree.cElementTree # S405
| ^^^^^^^^^^^^^^^^^^^^^^ S405
2 | from xml.etree import cElementTree # S405
3 | import xml.etree.ElementTree # S405
|
S405.py:2:6: S405 `xml.etree` methods are vulnerable to XML attacks
|
1 | import xml.etree.cElementTree # S405
2 | from xml.etree import cElementTree # S405
| ^^^^^^^^^ S405
3 | import xml.etree.ElementTree # S405
4 | from xml.etree import ElementTree # S405
|
S405.py:3:8: S405 `xml.etree` methods are vulnerable to XML attacks
|
1 | import xml.etree.cElementTree # S405
2 | from xml.etree import cElementTree # S405
3 | import xml.etree.ElementTree # S405
| ^^^^^^^^^^^^^^^^^^^^^ S405
4 | from xml.etree import ElementTree # S405
|
S405.py:4:6: S405 `xml.etree` methods are vulnerable to XML attacks
|
2 | from xml.etree import cElementTree # S405
3 | import xml.etree.ElementTree # S405
4 | from xml.etree import ElementTree # S405
| ^^^^^^^^^ S405
|

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S406.py:1:6: S406 `xml.sax` methods are vulnerable to XML attacks
|
1 | from xml import sax # S406
| ^^^ S406
2 | import xml.sax as xmls # S406
3 | import xml.sax # S406
|
S406.py:2:8: S406 `xml.sax` methods are vulnerable to XML attacks
|
1 | from xml import sax # S406
2 | import xml.sax as xmls # S406
| ^^^^^^^^^^^^^^^ S406
3 | import xml.sax # S406
|
S406.py:3:8: S406 `xml.sax` methods are vulnerable to XML attacks
|
1 | from xml import sax # S406
2 | import xml.sax as xmls # S406
3 | import xml.sax # S406
| ^^^^^^^ S406
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S407.py:1:6: S407 `xml.dom.expatbuilder` is vulnerable to XML attacks
|
1 | from xml.dom import expatbuilder # S407
| ^^^^^^^ S407
2 | import xml.dom.expatbuilder # S407
|
S407.py:2:8: S407 `xml.dom.expatbuilder` is vulnerable to XML attacks
|
1 | from xml.dom import expatbuilder # S407
2 | import xml.dom.expatbuilder # S407
| ^^^^^^^^^^^^^^^^^^^^ S407
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S408.py:1:6: S408 `xml.dom.minidom` is vulnerable to XML attacks
|
1 | from xml.dom.minidom import parseString # S408
| ^^^^^^^^^^^^^^^ S408
2 | import xml.dom.minidom # S408
|
S408.py:2:8: S408 `xml.dom.minidom` is vulnerable to XML attacks
|
1 | from xml.dom.minidom import parseString # S408
2 | import xml.dom.minidom # S408
| ^^^^^^^^^^^^^^^ S408
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S409.py:1:6: S409 `xml.dom.pulldom` is vulnerable to XML attacks
|
1 | from xml.dom.pulldom import parseString # S409
| ^^^^^^^^^^^^^^^ S409
2 | import xml.dom.pulldom # S409
|
S409.py:2:8: S409 `xml.dom.pulldom` is vulnerable to XML attacks
|
1 | from xml.dom.pulldom import parseString # S409
2 | import xml.dom.pulldom # S409
| ^^^^^^^^^^^^^^^ S409
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S410.py:1:8: S410 `lxml` is vulnerable to XML attacks
|
1 | import lxml # S410
| ^^^^ S410
2 | from lxml import etree # S410
|
S410.py:2:6: S410 `lxml` is vulnerable to XML attacks
|
1 | import lxml # S410
2 | from lxml import etree # S410
| ^^^^ S410
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S411.py:1:8: S411 XMLRPC is vulnerable to remote XML attacks
|
1 | import xmlrpc # S411
| ^^^^^^ S411
2 | from xmlrpc import server # S411
|
S411.py:2:6: S411 XMLRPC is vulnerable to remote XML attacks
|
1 | import xmlrpc # S411
2 | from xmlrpc import server # S411
| ^^^^^^ S411
|

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S412.py:1:6: S412 `httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided
|
1 | from twisted.web.twcgi import CGIScript # S412
| ^^^^^^^^^^^^^^^^^ S412
|

View File

@@ -0,0 +1,38 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S413.py:1:8: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
1 | import Crypto.Hash # S413
| ^^^^^^^^^^^ S413
2 | from Crypto.Hash import MD2 # S413
3 | import Crypto.PublicKey # S413
|
S413.py:2:6: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
1 | import Crypto.Hash # S413
2 | from Crypto.Hash import MD2 # S413
| ^^^^^^^^^^^ S413
3 | import Crypto.PublicKey # S413
4 | from Crypto.PublicKey import RSA # S413
|
S413.py:3:8: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
1 | import Crypto.Hash # S413
2 | from Crypto.Hash import MD2 # S413
3 | import Crypto.PublicKey # S413
| ^^^^^^^^^^^^^^^^ S413
4 | from Crypto.PublicKey import RSA # S413
|
S413.py:4:6: S413 `pycrypto` library is known to have publicly disclosed buffer overflow vulnerability
|
2 | from Crypto.Hash import MD2 # S413
3 | import Crypto.PublicKey # S413
4 | from Crypto.PublicKey import RSA # S413
| ^^^^^^^^^^^^^^^^ S413
|

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S415.py:1:8: S415 An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.
|
1 | import pyghmi # S415
| ^^^^^^ S415
2 | from pyghmi import foo # S415
|
S415.py:2:6: S415 An IPMI-related module is being imported. Prefer an encrypted protocol over IPMI.
|
1 | import pyghmi # S415
2 | from pyghmi import foo # S415
| ^^^^^^ S415
|

View File

@@ -0,0 +1,72 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S502.py:6:13: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv3`
|
4 | from OpenSSL.SSL import Context
5 |
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
|
S502.py:7:17: S502 Call made with insecure SSL protocol: `PROTOCOL_TLSv1`
|
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
|
S502.py:8:17: S502 Call made with insecure SSL protocol: `PROTOCOL_SSLv2`
|
6 | wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) # S502
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
|
S502.py:9:13: S502 Call made with insecure SSL protocol: `SSLv2_METHOD`
|
7 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) # S502
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
|
S502.py:10:13: S502 Call made with insecure SSL protocol: `SSLv23_METHOD`
|
8 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) # S502
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^^ S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
|
S502.py:11:9: S502 Call made with insecure SSL protocol: `SSLv3_METHOD`
|
9 | SSL.Context(method=SSL.SSLv2_METHOD) # S502
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
|
S502.py:12:9: S502 Call made with insecure SSL protocol: `TLSv1_METHOD`
|
10 | SSL.Context(method=SSL.SSLv23_METHOD) # S502
11 | Context(method=SSL.SSLv3_METHOD) # S502
12 | Context(method=SSL.TLSv1_METHOD) # S502
| ^^^^^^^^^^^^^^^^^^^^^^^ S502
13 |
14 | wrap_socket(ssl_version=ssl.PROTOCOL_TLS_CLIENT) # OK
|

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S503.py:6:18: S503 Argument default set to insecure SSL protocol: `PROTOCOL_SSLv2`
|
6 | def func(version=ssl.PROTOCOL_SSLv2): # S503
| ^^^^^^^^^^^^^^^^^^ S503
7 | pass
|
S503.py:10:19: S503 Argument default set to insecure SSL protocol: `SSLv2_METHOD`
|
10 | def func(protocol=SSL.SSLv2_METHOD): # S503
| ^^^^^^^^^^^^^^^^ S503
11 | pass
|
S503.py:14:18: S503 Argument default set to insecure SSL protocol: `SSLv23_METHOD`
|
14 | def func(version=SSL.SSLv23_METHOD): # S503
| ^^^^^^^^^^^^^^^^^ S503
15 | pass
|
S503.py:18:19: S503 Argument default set to insecure SSL protocol: `PROTOCOL_TLSv1`
|
18 | def func(protocol=PROTOCOL_TLSv1): # S503
| ^^^^^^^^^^^^^^ S503
19 | pass
|

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S504.py:4:1: S504 `ssl.wrap_socket` called without an `ssl_version``
|
2 | from ssl import wrap_socket
3 |
4 | ssl.wrap_socket() # S504
| ^^^^^^^^^^^^^^^^^ S504
5 | wrap_socket() # S504
6 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|
S504.py:5:1: S504 `ssl.wrap_socket` called without an `ssl_version``
|
4 | ssl.wrap_socket() # S504
5 | wrap_socket() # S504
| ^^^^^^^^^^^^^ S504
6 | ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) # OK
|

View File

@@ -51,13 +51,29 @@ pub(super) fn is_allowed_func_def(name: &str) -> bool {
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
return is_allowed_func_call(attr);
pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
let func_name = match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr.as_str(),
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
_ => return false,
};
// If the function name is explicitly allowed, then the boolean trap is
// allowed.
if is_allowed_func_call(func_name) {
return true;
}
if let Expr::Name(ast::ExprName { id, .. }) = func {
return is_allowed_func_call(id);
// If the function appears to be a setter (e.g., `set_visible` or `setVisible`), then the
// boolean trap is allowed. We want to avoid raising a violation for cases in which the argument
// is positional-only and third-party, and this tends to be the case for setters.
if call.arguments.args.len() == 1 {
if func_name
.strip_prefix("set")
.is_some_and(|suffix| suffix.starts_with(|c: char| c == '_' || c.is_ascii_uppercase()))
{
return true;
}
}
false

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,11 +45,16 @@ impl Violation for BooleanPositionalValueInCall {
}
}
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
if allow_boolean_trap(func) {
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, call: &ast::ExprCall) {
if allow_boolean_trap(call) {
return;
}
for arg in args.iter().filter(|arg| arg.is_boolean_literal_expr()) {
for arg in call
.arguments
.args
.iter()
.filter(|arg| arg.is_boolean_literal_expr())
{
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));

View File

@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
|
88 | # FBT001: Boolean positional arg in function definition
89 | def foo(self, value: bool) -> None:
90 | # FBT001: Boolean positional arg in function definition
91 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
90 | pass
92 | pass
|

View File

@@ -28,14 +28,10 @@ FBT.py:57:17: FBT003 Boolean positional value in function call
59 | mylist.index(True)
|
FBT.py:69:38: FBT003 Boolean positional value in function call
|
67 | os.set_blocking(0, False)
68 | g_action.set_enabled(True)
69 | settings.set_enable_developer_extras(True)
| ^^^^ FBT003
70 | foo.is_(True)
71 | bar.is_not(False)
|
FBT.py:121:10: FBT003 Boolean positional value in function call
|
121 | settings(True)
| ^^^^ FBT003
|

View File

@@ -81,26 +81,26 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
|
88 | # FBT001: Boolean positional arg in function definition
89 | def foo(self, value: bool) -> None:
90 | # FBT001: Boolean positional arg in function definition
91 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
90 | pass
92 | pass
|
FBT.py:99:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:101:10: FBT001 Boolean-typed positional argument in function definition
|
99 | def func(x: Union[list, Optional[int | str | float | bool]]):
101 | def func(x: Union[list, Optional[int | str | float | bool]]):
| ^ FBT001
100 | pass
102 | pass
|
FBT.py:103:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:105:10: FBT001 Boolean-typed positional argument in function definition
|
103 | def func(x: bool | str):
105 | def func(x: bool | str):
| ^ FBT001
104 | pass
106 | pass
|

View File

@@ -49,6 +49,31 @@ impl Violation for LoopVariableOverridesIterator {
}
}
/// B020
pub(crate) fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
let target_names = {
let mut target_finder = NameFinder::default();
target_finder.visit_expr(target);
target_finder.names
};
let iter_names = {
let mut iter_finder = NameFinder::default();
iter_finder.visit_expr(iter);
iter_finder.names
};
for (name, expr) in target_names {
if iter_names.contains_key(name) {
checker.diagnostics.push(Diagnostic::new(
LoopVariableOverridesIterator {
name: name.to_string(),
},
expr.range(),
));
}
}
}
#[derive(Default)]
struct NameFinder<'a> {
names: FxHashMap<&'a str, &'a Expr>,
@@ -97,28 +122,3 @@ where
}
}
}
/// B020
pub(crate) fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
let target_names = {
let mut target_finder = NameFinder::default();
target_finder.visit_expr(target);
target_finder.names
};
let iter_names = {
let mut iter_finder = NameFinder::default();
iter_finder.visit_expr(iter);
iter_finder.names
};
for (name, expr) in target_names {
if iter_names.contains_key(name) {
checker.diagnostics.push(Diagnostic::new(
LoopVariableOverridesIterator {
name: name.to_string(),
},
expr.range(),
));
}
}
}

View File

@@ -1,10 +1,9 @@
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers;
use ruff_python_ast::helpers::NameFinder;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{helpers, visitor};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -78,42 +77,16 @@ impl Violation for UnusedLoopControlVariable {
}
}
/// Identify all `Expr::Name` nodes in an AST.
struct NameFinder<'a> {
/// A map from identifier to defining expression.
names: FxHashMap<&'a str, &'a Expr>,
}
impl NameFinder<'_> {
fn new() -> Self {
NameFinder {
names: FxHashMap::default(),
}
}
}
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'a Expr) {
if let Expr::Name(ast::ExprName { id, .. }) = expr {
self.names.insert(id, expr);
}
visitor::walk_expr(self, expr);
}
}
/// B007
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
let control_names = {
let mut finder = NameFinder::new();
let mut finder = NameFinder::default();
finder.visit_expr(stmt_for.target.as_ref());
finder.names
};
let used_names = {
let mut finder = NameFinder::new();
let mut finder = NameFinder::default();
for stmt in &stmt_for.body {
finder.visit_stmt(stmt);
}

View File

@@ -1,11 +1,10 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Decorator};
use ruff_text_size::TextRange;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::SemanticModel;
use ruff_python_ast as ast;
use ruff_python_semantic::{BindingKind, Scope, ScopeId};
use ruff_source_file::SourceRow;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
@@ -20,6 +19,23 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// non-obvious errors, as readers may mistake the attribute for the
/// builtin and vice versa.
///
/// Since methods and class attributes typically cannot be referenced directly
/// from outside the class scope, this rule only applies to those methods
/// and attributes that both shadow a builtin _and_ are referenced from within
/// the class scope, as in the following example, where the `list[int]` return
/// type annotation resolves to the `list` method, rather than the builtin:
///
/// ```python
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
///
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// ```
///
/// Builtins can be marked as exceptions to this rule via the
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
/// converted to the appropriate dunder method. Methods decorated with
@@ -28,135 +44,112 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
///
/// ## Example
/// ```python
/// class Shadow:
/// def int():
/// return 0
/// ```
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
///
/// Use instead:
/// ```python
/// class Shadow:
/// def to_int():
/// return 0
/// ```
///
/// Or:
/// ```python
/// class Shadow:
/// # Callable as `int(shadow)`
/// def __int__():
/// return 0
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// ```
///
/// ## Options
/// - `flake8-builtins.builtins-ignorelist`
///
/// ## References
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
#[violation]
pub struct BuiltinAttributeShadowing {
kind: Kind,
name: String,
row: SourceRow,
}
impl Violation for BuiltinAttributeShadowing {
#[derive_message_formats]
fn message(&self) -> String {
let BuiltinAttributeShadowing { name } = self;
format!("Class attribute `{name}` is shadowing a Python builtin")
let BuiltinAttributeShadowing { kind, name, row } = self;
match kind {
Kind::Attribute => {
format!("Python builtin is shadowed by class attribute `{name}` from {row}")
}
Kind::Method => {
format!("Python builtin is shadowed by method `{name}` from {row}")
}
}
}
}
/// A003
pub(crate) fn builtin_attribute_shadowing(
checker: &mut Checker,
checker: &Checker,
scope_id: ScopeId,
scope: &Scope,
class_def: &ast::StmtClassDef,
name: &str,
range: TextRange,
diagnostics: &mut Vec<Diagnostic>,
) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
// subscripting and not through attribute access.
if class_def
.bases()
.iter()
.any(|base| checker.semantic().match_typing_expr(base, "TypedDict"))
{
return;
}
for (name, binding_id) in scope.all_bindings() {
let binding = checker.semantic().binding(binding_id);
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
// We only care about methods and attributes.
let kind = match binding.kind {
BindingKind::Assignment | BindingKind::Annotation => Kind::Attribute,
BindingKind::FunctionDefinition(_) => Kind::Method,
_ => continue,
};
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
// Class scopes are special, in that you can only reference a binding defined in a
// class scope from within the class scope itself. As such, we can safely ignore
// methods that weren't referenced from within the class scope. In other words, we're
// only trying to identify shadowing as in:
// ```python
// class Class:
// @staticmethod
// def list() -> None:
// pass
//
// @staticmethod
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
{
diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
kind,
name: name.to_string(),
row: checker.compute_source_row(binding.start()),
},
reference.range(),
));
}
}
}
}
/// 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,
checker.source_type,
) {
// 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,
semantic: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
return false;
};
match name {
// Ex) `Event.set`
"set" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter.filter`
"filter" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
}),
_ => false,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Kind {
Attribute,
Method,
}

View File

@@ -1,68 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
| ^^^^^^^^^^^ A003
3 | id: int
4 | dir = "/"
|
A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
3 | id: int
| ^^ A003
4 | dir = "/"
|
A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin
|
2 | ImportError = 4
3 | id: int
4 | dir = "/"
| ^^^ A003
5 |
6 | def __init__(self):
|
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
|
9 | self.dir = "."
10 |
11 | def str(self):
| ^^^ A003
12 | pass
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^ A003
18 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
A003.py:20:34: A003 Python builtin is shadowed by class attribute `id` from line 3
|
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
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^ A003
21 | pass
|

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