Compare commits

...

82 Commits

Author SHA1 Message Date
Zanie
35cc48a64c Add stubs for type params and type aliases 2023-07-17 19:06:16 -05:00
Zanie
0d4f1d86ad Format 2023-07-17 18:06:24 -05:00
Zanie
834910947e Update parser pin in fuzzer; fix lockfiles 2023-07-17 18:04:48 -05:00
Zanie
e34cfeb475 WIP: Add support for TypeAlias and TypeParam 2023-07-17 17:52:59 -05:00
Zanie
bfaa1f9530 Bump RustPython-Parser to include PEP-695
126652b684
2023-07-17 17:52:06 -05:00
David Szotten
52aa2fc875 upgrade rustpython to remove tuple-constants (#5840)
c.f. https://github.com/astral-sh/RustPython-Parser/pull/28

Tests: No snapshots changed

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-07-17 22:50:31 +00:00
Charlie Marsh
e574a6a769 Add some "Phase" annotations to other visit methods (#5839)
## Summary

Follow-up from #5820.
2023-07-17 14:46:39 -04:00
Charlie Marsh
b9346a4fd6 Draw boundaries between various Checker visitation phases (#5820)
## Summary

This PR does some non-behavior-changing refactoring of the AST checker.
Specifically, it breaks the `Stmt`, `Expr`, and `ExceptHandler` visitors
into four distinct, consistent phases:

1. **Phase 1: Analysis**: Run any lint rules on the node.
2. **Phase 2: Binding**: Bind any symbols declared by the node.
3. **Phase 3: Recursion**: Visit all child nodes.
4. **Phase 4: Clean-up**: Pop scopes, etc.

There are some fuzzy boundaries in the last three phases, but the most
important divide is between the Phase 1 and all the others -- the goal
here is (as much as possible) to disentangle all of the vanilla
lint-rule calls from any other semantic analysis or model building.

Part of the motivation here is that I'm considering re-ordering some of
these phases, and it was just impossible to reason about that change as
long as we had miscellaneous binding-creation and scope-modification
code intermingled with lint rules. However, this could also enable us to
(e.g.) move the entire analysis phase elsewhere, and even with a more
limited API that has read-only access to `Checker` (but can push to a
diagnostics vector).
2023-07-17 13:02:21 -04:00
Charlie Marsh
8001a2f121 Expand convention documentation (#5819) 2023-07-17 14:12:46 +00:00
konsti
7dd30f0270 Read black options in format_dev script (#5827)
## Summary

Comparing repos with black requires that we use the settings as black,
notably line length and magic trailing comma behaviour. Excludes and
preserving quotes (vs. a preference for either quote style) is not yet
implemented because they weren't needed for the test projects.

In the other two commits i fixed the output when the progress bar is
hidden (this way is recommonded in the indicatif docs), added a
`scratch.pyi` file to gitignore because black formats stub files
differently and also updated the ecosystem readme with the projects json
without forks.

## Test Plan

I added a `line-length` vs `line_length` test. Otherwise only my
personal usage atm, a PR to integrate the script into the CI to check
some projects will follow.
2023-07-17 13:29:43 +00:00
Micha Reiser
21063544f7 Fix formatter generate.py (#5829) 2023-07-17 10:41:27 +00:00
Luc Khai Hai
fb336898a5 Format AsyncFor (#5808) 2023-07-17 10:38:59 +02:00
Tom Kuson
f5f8eb31ed Add documentation to the flake8-gettext (INT) rules (#5813)
## Summary

Completes documentation for the `flake8-gettext` (`INT`) ruleset.
Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-07-17 04:09:33 +00:00
Charlie Marsh
be6c744856 Include function name in undocumented-param message (#5818)
Closes #5814.
2023-07-16 22:51:34 -04:00
Charlie Marsh
94998aedef Reduce unnecessary allocations for keyword detection (#5817) 2023-07-17 02:22:30 +00:00
Tom Kuson
1c0376a72d Add documentation to the S5XX rules (#5805)
## Summary

Add documentation to the `S5XX` rules (the `flake8-bandit`
['cryptography'](https://bandit.readthedocs.io/en/latest/plugins/index.html#plugin-id-groupings)
rule group). Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-07-17 02:12:57 +00:00
Simon Brugman
de2a13fcd7 [pandas-vet] series constant series (#5802)
## Summary

Implementation for https://github.com/astral-sh/ruff/issues/5588

Q1: are there any additional semantic helpers that could be used to
guard this rule? Which existing rules should be similar in that respect?
Can we at least check if `pandas` is imported (any pointers welcome)?
Currently, the rule flags:
```python
data = {"a": "b"}
data.nunique() == 1
```

Q2: Any pointers on naming of the rule and selection of the code? It was
proposed, but not replied to/implemented in the upstream. `pandas` did
accept a PR to update their cookbook to reflect this rule though.

## Test Plan

TODO:
- [X] Checking for ecosystem CI results
- [x] Test on selected [real-world
cases](https://github.com/search?q=%22nunique%28%29+%3D%3D+1%22+language%3APython+&type=code)
  - [x] https://github.com/sdv-dev/SDMetrics
  - [x] https://github.com/google-research/robustness_metrics
  - [x] https://github.com/soft-matter/trackpy
  - [x] https://github.com/microsoft/FLAML/
- [ ] Add guarded test cases
2023-07-17 01:55:34 +00:00
Harutaka Kawamura
cfec636046 Do not fix NamedTuple calls containing both a list of fields and keywords (#5799)
## Summary

Fixes #5794

## Test Plan

Existing tests
2023-07-17 01:31:53 +00:00
Tom Kuson
ae431df146 Change pandas-use-of-dot-read-table rule to emit only when read_table is used on CSV data (#5807)
## Summary

Closes #5628 by only emitting if `sep=","`. Includes documentation
(completes the `pandas-vet` ruleset).

Related to #2646.

## Test Plan

`cargo test`
2023-07-17 01:25:13 +00:00
Charlie Marsh
2cd117ba81 Remove TryIdentifier trait (#5816)
## Summary

Last remaining usage here is for patterns, but we now have ranges on
identifiers so it's unnecessary.
2023-07-16 21:24:16 -04:00
Simon Brugman
a956226d95 perf: only compute start offset for overlong lines (#5811)
Moves the computation of the `start_offset` for overlong lines to just
before the result is returned. There is a slight overhead for overlong
lines (double the work for the first `limit` characters).

In practice this results in a speedup on the CPython codebase. Most
lines are not overlong, or are not enforced because the line ends with a
URL, or does not contain whitespace. Nonetheless, the 0.3% of overlong
lines are a lot compared to other violations.

### Before
![selected
before](https://github.com/astral-sh/ruff/assets/9756388/d32047df-7fd2-4ae8-8333-1a3679ce000f)
_Selected W505 and E501_

![all
before](https://github.com/astral-sh/ruff/assets/9756388/98495118-c474-46ff-873c-fb58a78cfe15)
_All rules_

### After
![selected
after](https://github.com/astral-sh/ruff/assets/9756388/e4bd7f10-ff7e-4d52-8267-27cace8c5471)
_Selected W505 and E501_

![all
after](https://github.com/astral-sh/ruff/assets/9756388/573bdbe2-c64f-4f22-9659-c68726ff52c0)
_All rules_

CPython line statistics:
- Number of Python lines: 867.696
- Number of overlong lines: 2.963 (0.3%)

<details>

Benchmark selected:
```shell
cargo build --release && hyperfine --warmup 10 --min-runs 50 \                                                  
  "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e --select W505,E501"
```

Benchmark all:
```shell
cargo build --release && hyperfine --warmup 10 --min-runs 50 \                                                  
  "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e --select ALL"
```

Overlong lines in CPython

```shell
cargo run -p ruff_cli -- check crates/ruff/resources/test/cpython/Lib --no-cache --select=E501,W505 --statistics
```

Total Python lines:
```shell
find crates/ruff/resources/test/cpython/ -name '*.py' | xargs wc -l
```

</details>

(Performance tested on Mac M1)
2023-07-16 21:05:44 -04:00
Chris Pryer
1dd52ad139 Update generate.py comment (#5809)
## Summary

The generated comment is different from the generate files current
comment.

## Test Plan

None
2023-07-16 11:51:30 -04:00
Charlie Marsh
d692ed0896 Use a match statement for builtin detection (#5798)
## Summary

We've seen speed-ups in the past by converting from slice iteration to
match statements; this just does the same for built-in checks.
2023-07-16 04:57:57 +00:00
Charlie Marsh
01b05fe247 Remove Identifier usages for isolating exception names (#5797)
## Summary

The motivating change here is to remove `let range =
except_handler.try_identifier().unwrap();` and instead just do
`name.range()`, since exception names now have ranges attached to them
by the parse. This also required some refactors (which are improvements)
to the built-in attribute shadowing rules, since at least one invocation
relied on passing in the exception handler and calling
`.try_identifier()`. Now that we have easy access to identifiers, we can
remove the whole `AnyShadowing` abstraction.
2023-07-16 04:49:48 +00:00
Charlie Marsh
59dfd0e793 Move except-handler flag into visit_except_handler (#5796)
## Summary

This is more similar to how these flags work in other contexts (e.g.,
`visit_annotation`), and also ensures that we unset it prior to visit
the `orelse` and `finalbody` (a subtle bug).
2023-07-16 00:35:02 -04:00
Charlie Marsh
c7ff743d30 Use semantic().global() to power global-statement rule (#5795)
## Summary

The intent of this rule is to always flag the `global` declaration, not
the usage. The current implementation does the wrong thing if a global
is assigned multiple times. Using `semantic().global()` is also more
efficient.
2023-07-16 00:34:42 -04:00
konsti
b01a4d8446 Update ruff crate descriptions (#5710)
## Summary

I updated all ruff crate descriptions in the contributing guide

## Test Plan

n/a
2023-07-16 02:41:47 +00:00
Justin Prieto
f012ed2d77 Add autofix for B004 (#5788)
## Summary

Adds autofix for `hasattr` case of B004. I don't think it's safe (or
simple) to implement it for the `getattr` case because, inter alia,
calling `getattr` may have side effects.

Fixes #3545

## Test Plan

Existing tests were sufficient. Updated snapshots
2023-07-16 01:32:21 +00:00
Charlie Marsh
06b5c6c06f Use SmallVec#extend_from_slice in lieu of SmallVec#extend (#5793)
## Summary

There's a note in the docs that suggests this can be faster, and in the
benchmarks it... seems like it is? Might just be noise but held up over
a few runs.

Before:

<img width="1792" alt="Screen Shot 2023-07-15 at 9 10 06 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/973cd955-d4e6-4ae3-898e-90b7eb52ecf2">

After:

<img width="1792" alt="Screen Shot 2023-07-15 at 9 10 09 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/1491b391-d219-48e9-aa47-110bc7dc7f90">
2023-07-15 21:25:12 -04:00
Charlie Marsh
4782675bf9 Remove lexer-based comment range detection (#5785)
## Summary

I'm doing some unrelated profiling, and I noticed that this method is
actually measurable on the CPython benchmark -- it's > 1% of execution
time. We don't need to lex here, we already know the ranges of all
comments, so we can just do a simple binary search for overlap, which
brings the method down to 0%.

## Test Plan

`cargo test`
2023-07-16 01:03:27 +00:00
Charlie Marsh
f2e995f78d Gate runtime-import-in-type-checking-block (TCH004) behind enabled flag (#5789)
Closes #5787.
2023-07-15 20:57:29 +00:00
guillaumeLepape
6824b67f44 Include alias when formatting import-from structs (#5786)
## Summary

When required-imports is set with the syntax from ... import ... as ...,
autofix I002 is failing

## Test Plan

Reuse the same python files as
`crates/ruff/src/rules/isort/mod.rs:required_import` test.
2023-07-15 15:53:21 -04:00
Charlie Marsh
8ccd697020 Expand scope of quoted-annotation rule (#5766)
## Summary

Previously, the `quoted-annotation` rule only removed quotes when `from
__future__ import annotations` was present. However, there are some
other cases in which this is also safe -- for example:

```python
def foo():
    x: "MyClass"
```

We already model these in the semantic model, so this PR just expands
the scope of the rule to handle those.
2023-07-15 15:37:34 -04:00
Charlie Marsh
2de6f30929 Lift Expr::Subscript value visit out of branches (#5783)
Like #5772, but for subscripts.
2023-07-15 15:12:15 -04:00
Micha Reiser
df2efe81c8 Respect magic trailing comma for set expression (#5782)
<!--
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

This PR uses the `join_comma_separated` builder for formatting set
expressions
to ensure the formatting preserves magic commas, if the setting is
enabled.
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
See the fixed black tests

<!-- How was it tested? -->
2023-07-15 16:40:38 +00:00
Chris Pryer
fa4855e6fe Format DictComp expression (#5771)
## Summary

Format `DictComp` like `ListComp` from #5600. It's not 100%, but I
figured maybe it's worth starting to explore.

## Test Plan

Added ruff fixture based on `ListComp`'s.
2023-07-15 17:35:23 +01:00
Micha Reiser
3cda89ecaf Parenthesize with statements (#5758)
<!--
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

This PR improves the parentheses handling for with items to get closer
to black's formatting.

### Case 1:

```python
# Black / Input
with (
    [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbb",
        "cccccccccccccccccccccccccccccccccccccccccc",
        dddddddddddddddddddddddddddddddd,
    ] as example1,
    aaaaaaaaaaaaaaaaaaaaaaaaaa
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    + cccccccccccccccccccccccccccc
    + ddddddddddddddddd as example2,
    CtxManager2() as example2,
    CtxManager2() as example2,
    CtxManager2() as example2,
):
    ...

# Before
with (
    [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbb",
        "cccccccccccccccccccccccccccccccccccccccccc",
        dddddddddddddddddddddddddddddddd,
    ] as example1,
    (
        aaaaaaaaaaaaaaaaaaaaaaaaaa
        + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
        + cccccccccccccccccccccccccccc
        + ddddddddddddddddd
    ) as example2,
    CtxManager2() as example2,
    CtxManager2() as example2,
    CtxManager2() as example2,
):
    ...
```

Notice how Ruff wraps the binary expression in an extra set of
parentheses


### Case 2:
Black does not expand the with-items if the with has no parentheses:

```python
# Black / Input
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
    ...

# Before
with (
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c
):
    ...
```

Or 

```python
# Black / Input
with [
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "bbbbbbbbbb",
    "cccccccccccccccccccccccccccccccccccccccccc",
    dddddddddddddddddddddddddddddddd,
] as example1, aaaaaaaaaaaaaaaaaaaaaaaaaa * bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccccccc + ddddddddddddddddd as example2, CtxManager222222222222222() as example2:
    ...

# Before (Same as Case 1)
with (
    [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbb",
        "cccccccccccccccccccccccccccccccccccccccccc",
        dddddddddddddddddddddddddddddddd,
    ] as example1,
    (
        aaaaaaaaaaaaaaaaaaaaaaaaaa
        * bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
        * cccccccccccccccccccccccccccc
        + ddddddddddddddddd
    ) as example2,
    CtxManager222222222222222() as example2,
):
    ...

```
## Test Plan

I added new snapshot tests

Improves the django similarity index from 0.973 to 0.977
2023-07-15 16:03:09 +01:00
Luc Khai Hai
e1c119fde3 Format SetComp (#5774)
<!--
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

Format `SetComp` like `ListComp`.

## Test Plan

Derived from `ListComp`'s fixture.
2023-07-15 15:50:47 +01:00
Harutaka Kawamura
daa4b72d5f [B006] Add bytes to immutable types (#5776)
## Summary

`B006` should allow using `bytes(...)` as an argument defaule value.

## Test Plan

A new test case

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2023-07-15 13:04:33 +00:00
Charlie Marsh
f029f8b784 Move function visit out of Expr::Call branches (#5772)
## Summary

Non-behavioral change, but this is the same in each branch. Visiting the
`func` first also means we've visited the `func` by the time we try to
resolve it (via `resolve_call_path`), which should be helpful in a
future refactor.
2023-07-15 03:36:19 +00:00
Charlie Marsh
bf248ede93 Handle name nodes prior to running rules (#5770)
## Summary

This is more consistent with other patterns in the Checker. Shouldn't
change behavior at all.
2023-07-15 02:21:55 +00:00
Charlie Marsh
086f8a3c12 Move lambda visitation into recurse phase (#5769)
## Summary

Similar to #5768: when we analyze a lambda, we need to recurse in the
recurse phase, rather than the pre-visit phase.
2023-07-15 02:11:47 +00:00
Charlie Marsh
3dc73395ea Move Literal flag detection into recurse phase (#5768)
## Summary

The AST pass is broken up into three phases: pre-visit (which includes
analysis), recurse (visit all members), and post-visit (clean-up). We're
not supposed to edit semantic model flags in the pre-visit phase, but it
looks like we were for literal detection. This didn't matter in
practice, but I'm looking into some AST refactors for which this _does_
cause issues.

No behavior changes expected.

## Test Plan

Good test coverage on these.
2023-07-15 02:04:15 +00:00
Charlie Marsh
7c32e98d10 Use unused variable detection to power incorrect-dict-iterator (#5763)
## Summary

`PERF102` looks for unused keys or values in `dict.items()` calls, and
suggests instead using `dict.keys()` or `dict.values()`. Previously,
this check determined usage by looking for underscore-prefixed
variables. However, we can use the semantic model to actually detect
whether a variable is used. This has two nice effects:

1. We avoid odd false-positives whereby underscore-prefixed variables
are actually used.
2. We can catch more cases (fewer false-negatives) by detecting unused
loop variables that _aren't_ underscore-prefixed.

Closes #5692.
2023-07-14 15:42:47 -04:00
Charlie Marsh
81b88dcfb9 Misc. minor refactors to incorrect-dict-iterator (#5762)
## Summary

Mostly a no-op: use a single match for key-value, use identifier range
rather than re-lexing, respect our `dummy-variable-rgx` setting.
2023-07-14 17:29:25 +00:00
Micha Reiser
8187bf9f7e Cover Black's is_aritmetic_like formatting (#5738) 2023-07-14 17:54:58 +02:00
Charlie Marsh
513de13c46 Remove B904's lowercase exemption (#5751)
## Summary

It looks like bugbear, [from the
start](https://github.com/PyCQA/flake8-bugbear/pull/181#issuecomment-904314876),
has had an exemption here to exempt `raise lower_case_var`. I looked at
Hypothesis and Trio, which are mentioned in that issue, and Hypothesis
has exactly one case of this, and Trio has none, so IMO it doesn't seem
worth special-casing.

Closes https://github.com/astral-sh/ruff/issues/5664.
2023-07-14 11:46:21 -04:00
Justin Prieto
816f7644a9 Fix nested calls to sorted with differing arguments (#5761)
## Summary

Nested calls to `sorted` can only be collapsed if the calls are
identical (i.e., they have the exact same keyword arguments).
Update C414 to only flag such cases.

Fixes #5712

## Test Plan

Updated snapshots.
Tested against flake8-comprehensions. It incorrectly flags these cases.
2023-07-14 13:43:47 +00:00
konsti
fb46579d30 Add Regression test for #5605, where formatting x[:,] failed. (#5759)
#5605 has been fixed, i added the failing example from the issue as a
regression test.

Closes #5605
2023-07-14 11:55:05 +02:00
Chris Pryer
a961f75e13 Format assert statement (#5168) 2023-07-14 09:01:33 +02:00
Charlie Marsh
5a4516b812 Misc. stylistic changes from flipping through rules late at night (#5757)
## Summary

This is really bad PR hygiene, but a mix of: using `Locator`-based fixes
in a few places (in lieu of `Generator`-based fixes), using match syntax
to avoid `.len() == 1` checks, using common helpers in more places, etc.

## Test Plan

`cargo test`
2023-07-14 05:23:47 +00:00
Charlie Marsh
875e04e369 Avoid removing raw strings in comparison fixes (#5755)
## Summary

Use `Locator`-based verbatim fix rather than a `Generator`-based fix,
which loses trivia (and raw strings).

Closes https://github.com/astral-sh/ruff/issues/4130.
2023-07-14 04:27:46 +00:00
Charlie Marsh
12489d3305 Minor tweaks to playground color scheme (#5754)
## Summary

I kind of hate the light mode theme, but they now use colors from our
actual palette:

<img width="1792" alt="Screen Shot 2023-07-13 at 10 15 14 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/f1da0153-d6ed-4b65-9419-b824f2cad614">
<img width="1792" alt="Screen Shot 2023-07-13 at 10 15 12 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/d9452e10-796b-4b7f-bf3f-7af6e0b14fc0">
<img width="1792" alt="Screen Shot 2023-07-13 at 10 15 10 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/f75e7c1c-3b5a-4a78-8bb8-d8b4d40a337d">
<img width="1792" alt="Screen Shot 2023-07-13 at 10 15 07 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/52c23108-b9c2-4a1f-adf0-e11098dbdc5d">
2023-07-13 22:37:18 -04:00
Charlie Marsh
73228e914c Use Ruff favicon for playground (#5752) 2023-07-14 01:11:44 +00:00
Charlie Marsh
af2a087806 Ignore Enum-and-str subclasses for slots enforcement (#5749)
## Summary

Matches the behavior of the upstream plugin.

Closes #5748.
2023-07-13 20:12:16 +00:00
Charlie Marsh
51a313cca4 Avoid stack overflow for non-BitOr binary types (#5743)
## Summary

Closes #5742.
2023-07-13 14:23:40 -04:00
skykasko
48309cad08 Fix the example for blank-line-before-class (D211) (#5746)
The example for
[D211](https://beta.ruff.rs/docs/rules/blank-line-before-class/) is
currently identical to the example for
[D203](https://beta.ruff.rs/docs/rules/one-blank-line-before-class/). It
should be the opposite, with the incorrect case having a blank line
before the class docstring and the correct case having no blank line.
2023-07-13 17:47:01 +00:00
Charlie Marsh
2c2e5b2704 Add some additional Option links to the docs (#5745) 2023-07-13 13:46:17 -04:00
Dhruv Manilawala
5d135d4e0e Update table of content in CONTRIBUTING.md (#5744) 2023-07-13 17:42:28 +00:00
eggplants
06a04c10e2 Fix Options section of rule docs (#5741)
## Summary

Fix: #5740

A trailing line-break are needed for the anchor.

## Test Plan

http://127.0.0.1:8000/docs/rules/line-too-long/#options

|before|after|
|--|--|

|![image](https://github.com/astral-sh/ruff/assets/42153744/8cb9dcce-aeda-4255-b21e-ab11817ba9e1)|![image](https://github.com/astral-sh/ruff/assets/42153744/b68d4fd7-da5a-4494-bb95-f7792f1a42db)|
2023-07-13 17:25:54 +00:00
Charlie Marsh
fee0f43925 Add an overview of Ruff's compilation pipeline to the docs (#5719)
## Summary

I originally wrote this in Notion but it seems preferable to publish it
publicly in the documentation. Feedback welcome!
2023-07-13 16:50:41 +00:00
Justin Prieto
25e491ad6f [flake8-pyi] Implement PYI041 (#5722)
## Summary

Implements PYI041 from flake8-pyi. See [original
code](2a86db8271/pyi.py (L1283)).

This check only applies to function parameters in order to avoid issues
with mypy. See https://github.com/PyCQA/flake8-pyi/issues/299.

ref: #848

## Test Plan

Snapshots, manual runs of flake8.
2023-07-13 16:48:17 +00:00
Charlie Marsh
e7b059cc5c Fix nested lists in CONTRIBUTING.md (#5721)
## Summary

We have a lot of two-space-indented stuff, but apparently it needs to be
four-space indented to render as expected in MkDocs.
2023-07-13 16:32:59 +00:00
Micha Reiser
5dd5ee0c5b Properly group assignment targets (#5728) 2023-07-13 16:00:49 +02:00
konsti
f48ab2d621 Update scripts/ecosystem_all_check.sh (#5737)
## Summary

These changes make `scripts/ecosystem_all_check.sh --select ALL` work
again, i forgot to update this script to the new directory structure
from #5299 because it's only run manually


## Test Plan

n/a
2023-07-13 15:25:22 +02:00
Dhruv Manilawala
cf48ad7b21 Consider single element subscript expr for implicit optional (#5717)
## Summary

Consider single element subscript expr for implicit optional.

On `main`, the cases where there is only a single element in the
subscript
list was giving false positives such as for the following:

```python
typing.Union[None]
typing.Literal[None]
```

## Test Plan

`cargo test`

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-07-13 13:10:07 +00:00
Dhruv Manilawala
f44acc047a Check for Any in other types for ANN401 (#5601)
## Summary

Check for `Any` in other types for `ANN401`. This reuses the logic from
`implicit-optional` rule to resolve the type to `Any`.

Following types are supported:
* `Union[Any, ...]`
* `Any | ...`
* `Optional[Any]`
* `Annotated[<any of the above variant>, ...]`
* Forward references i.e., `"Any | ..."`

## Test Plan

Added test cases for various combinations.

fixes: #5458
2023-07-13 18:19:27 +05:30
Tom Kuson
8420008e79 Avoid checking EXE001 and EXE002 on WSL (#5735)
## Summary

Do not raise `EXE001` and `EXE002` if WSL is detected. Uses the
[`wsl`](https://crates.io/crates/wsl) crate.

Closes #5445.

## Test Plan

`cargo test`

I don't use Windows, so was unable to test on a WSL environment. It
would be good if someone who runs Windows could check the functionality.
2023-07-13 07:36:07 -04:00
Charlie Marsh
932c9a4789 Extend PEP 604 rewrites to support some quoted annotations (#5725)
## Summary

Python doesn't allow `"Foo" | None` if the annotation will be evaluated
at runtime (see the comments in the PR, or the semantic model
documentation for more on what this means and when it is true), but it
_does_ allow it if the annotation is typing-only.

This, for example, is invalid, as Python will evaluate `"Foo" | None` at
runtime in order to
populate the function's `__annotations__`:

```python
def f(x: "Foo" | None): ...
```

This, however, is valid:

```python
def f():
    x: "Foo" | None
```

As is this:

```python
from __future__ import annotations

def f(x: "Foo" | None): ...
```

Closes #5706.
2023-07-13 07:34:04 -04:00
konsti
549173b395 Fix StmtAnnAssign formatting by mirroring StmtAssign (#5732)
## Summary

`StmtAnnAssign` would not insert parentheses when breaking the same way
`StmtAssign` does, causing unstable formatting and likely some syntax
errors.

## Test Plan

I added a regression test.
2023-07-13 10:51:25 +00:00
konsti
b1781abffb Link issue tracker in contributing docs (#5688)
## Summary

This adds links to issue categories that are good for people looking to
implement something and a link to the contributing guide feedback issue
(https://github.com/astral-sh/ruff/issues/5684)

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-07-13 10:42:09 +00:00
konsti
68e0f97354 Formatter: Better f-string dummy (#5730)
## Summary

The previous dummy was causing instabilities since it turned a string
into a variable.

E.g.
```python
            script_header_dict[
                "slurm_partition_line"
            ] = f"#SBATCH --partition {resources.queue_name}"
```
has an instability as
```python
-            script_header_dict["slurm_partition_line"] = (
-                NOT_YET_IMPLEMENTED_ExprJoinedStr
-            )
+            script_header_dict[
+                "slurm_partition_line"
+            ] = NOT_YET_IMPLEMENTED_ExprJoinedStr
```

## Test Plan

The instability is gone, otherwise it's still a dummy
2023-07-13 09:27:25 +00:00
Dhruv Manilawala
e9771c9c63 Ignore Jupyter Notebooks for --add-noqa (#5727) 2023-07-13 13:26:47 +05:30
Micha Reiser
067b2a6ce6 Pass parent to NeedsParentheses (#5708) 2023-07-13 08:57:29 +02:00
Charlie Marsh
30702c2977 Flatten nested tuples when fixing UP007 violations (#5724)
## Summary

Also upgrading these to "Suggested" from "Manual" (they should've always
been "Suggested", I think), and adding some more test cases.
2023-07-13 04:11:32 +00:00
Charlie Marsh
34b79ead3d Use Locator-based replacement rather than Generator for UP007 (#5723)
## Summary

Locator-based replacement is generally preferable as we get verbatim
fixes.
2023-07-13 03:50:16 +00:00
Justin Prieto
19f475ae1f [flake8-pyi] Implement PYI036 (#5668)
## Summary

Implements PYI036 from `flake8-pyi`. See [original
code](https://github.com/PyCQA/flake8-pyi/blob/main/pyi.py#L1585)

## Test Plan

- Updated snapshots
- Checked against manual runs of flake8

ref: #848
2023-07-13 02:50:00 +00:00
Tom Kuson
2b03bd18f4 Implement Pylint consider-using-in (#5193)
## Summary

Implement Pylint rule [`consider-using-in`
(`R1714`)](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/consider-using-in.html)
as `repeated-equality-comparison-target` (`PLR1714`). This rule checks
for expressions that can be re-written as a membership test for better
readability and performance.

For example,

```python
foo == "bar" or foo == "baz" or foo == "qux"
```

should be rewritten as

```python
foo in {"bar", "baz", "qux"}
```

Related to #970. Includes documentation.

### Implementation quirks

The implementation does not work with Yoda conditions (e.g., `"a" ==
foo` instead of `foo == "a"`). The Pylint version does. I couldn't find
a way of supporting Yoda-style conditions without it being inefficient,
so didn't (I don't think people write Yoda conditions any way).

## Test Plan

Added fixture.

`cargo test`
2023-07-13 01:32:34 +00:00
Charlie Marsh
c87faca884 Use Cursor for shebang parsing (#5716)
## Summary

Better to leverage the shared functionality we get from `Cursor`. It's
also a little bit faster, which is very cool.
2023-07-12 21:22:09 +00:00
Charlie Marsh
6dbc6d2e59 Use shared Cursor across crates (#5715)
## Summary

We have two `Cursor` implementations. This PR moves the implementation
from the formatter into `ruff_python_whitespace` (kind of a poorly-named
crate now) and uses it for both use-cases.
2023-07-12 21:09:27 +00:00
Charlie Marsh
6ce252f0ed Tweak hierarchy of benchmark docs (#5720)
## Summary

Before:

<img width="309" alt="Screen Shot 2023-07-12 at 4 33 23 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/b4a29dc5-183d-479f-8028-f47157b87e0e">

After:

<img width="281" alt="Screen Shot 2023-07-12 at 4 33 32 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/316859d3-db90-4595-8c07-b4bb6543ac4d">
2023-07-12 17:08:22 -04:00
Charlie Marsh
c029c8b37a Run release testing on PR, not push (#5718)
## Summary

This job runs whenever I put up a PR to bump the version, which is
really useful. But then it also runs again when I merge, and then _that_
job tends to get cancelled immediately, because I run the _actual_
release job, which triggers the cancel-concurrent-runs flow. (See, e.g.,
https://github.com/astral-sh/ruff/actions/runs/5534191373.)

I think it makes sense to run these on PR (when editing `pyproject.toml`
and friends), but not again on merge.
2023-07-12 14:22:29 -04:00
342 changed files with 9793 additions and 6618 deletions

View File

@@ -9,7 +9,7 @@ on:
sha:
description: "Optionally, the full sha of the commit to be released"
type: string
push:
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
- pyproject.toml

2
.gitignore vendored
View File

@@ -10,7 +10,7 @@ schemastore
# `maturin develop` and ecosystem_all_check.sh
.venv*
# Formatter debugging (crates/ruff_python_formatter/README.md)
scratch.py
scratch.*
# Created by `perf` (CONTRIBUTING.md)
perf.data
perf.data.old

View File

@@ -1,6 +1,10 @@
# default to true for all rules
default: true
# MD007/unordered-list-indent
MD007:
indent: 4
# MD033/no-inline-html
MD033: false
@@ -8,7 +12,4 @@ MD033: false
MD041: false
# MD013/line-length
MD013:
line_length: 100
code_blocks: false
ignore_code_blocks: true
MD013: false

View File

@@ -22,6 +22,7 @@ repos:
hooks:
- id: mdformat
additional_dependencies:
- mdformat-mkdocs
- mdformat-black
- black==23.1.0 # Must be the latest version of Black

View File

@@ -48,12 +48,12 @@ Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting n
follows:
- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore
`UP006` violations, even if `from __future__ import annotations` is present in the file.
While such annotations are valid in Python 3.7 and Python 3.8 when combined with
`from __future__ import annotations`, they aren't supported by libraries like Pydantic and
FastAPI, which rely on runtime type checking.
`UP006` violations, even if `from __future__ import annotations` is present in the file.
While such annotations are valid in Python 3.7 and Python 3.8 when combined with
`from __future__ import annotations`, they aren't supported by libraries like Pydantic and
FastAPI, which rely on runtime type checking.
- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation,
and libraries like Pydantic and FastAPI support it without issue.
and libraries like Pydantic and FastAPI support it without issue.
In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations
that are not supported at runtime by the current Python version, which are unsupported by libraries
@@ -203,25 +203,25 @@ This change is largely backwards compatible -- most users should experience
no change in behavior. However, please note the following exceptions:
- Subcommands will now fail when invoked with unsupported arguments, instead
of silently ignoring them. For example, the following will now fail:
of silently ignoring them. For example, the following will now fail:
```console
ruff --clean --respect-gitignore
```
```console
ruff --clean --respect-gitignore
```
(the `clean` command doesn't support `--respect-gitignore`.)
(the `clean` command doesn't support `--respect-gitignore`.)
- The semantics of `ruff <arg>` have changed slightly when `<arg>` is a valid subcommand.
For example, prior to this release, running `ruff rule` would run `ruff` over a file or
directory called `rule`. Now, `ruff rule` would invoke the `rule` subcommand. This should
only impact projects with files or directories named `rule`, `check`, `explain`, `clean`,
or `generate-shell-completion`.
For example, prior to this release, running `ruff rule` would run `ruff` over a file or
directory called `rule`. Now, `ruff rule` would invoke the `rule` subcommand. This should
only impact projects with files or directories named `rule`, `check`, `explain`, `clean`,
or `generate-shell-completion`.
- Scripts that invoke ruff should supply `--` before any positional arguments.
(The semantics of `ruff -- <arg>` have not changed.)
(The semantics of `ruff -- <arg>` have not changed.)
- `--explain` previously treated `--format grouped` as a synonym for `--format text`.
This is no longer supported; instead, use `--format text`.
This is no longer supported; instead, use `--format text`.
## 0.0.226

View File

@@ -6,10 +6,10 @@
- [Scope](#scope)
- [Enforcement](#enforcement)
- [Enforcement Guidelines](#enforcement-guidelines)
- [1. Correction](#1-correction)
- [2. Warning](#2-warning)
- [3. Temporary Ban](#3-temporary-ban)
- [4. Permanent Ban](#4-permanent-ban)
- [1. Correction](#1-correction)
- [2. Warning](#2-warning)
- [3. Temporary Ban](#3-temporary-ban)
- [4. Permanent Ban](#4-permanent-ban)
- [Attribution](#attribution)
## Our Pledge
@@ -33,20 +33,20 @@ community include:
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Enforcement Responsibilities

View File

@@ -3,16 +3,29 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
- [The Basics](#the-basics)
- [Prerequisites](#prerequisites)
- [Development](#development)
- [Project Structure](#project-structure)
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
- [Rule naming convention](#rule-naming-convention)
- [Rule testing: fixtures and snapshots](#rule-testing-fixtures-and-snapshots)
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [Prerequisites](#prerequisites)
- [Development](#development)
- [Project Structure](#project-structure)
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
- [Rule naming convention](#rule-naming-convention)
- [Rule testing: fixtures and snapshots](#rule-testing-fixtures-and-snapshots)
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [MkDocs](#mkdocs)
- [Release Process](#release-process)
- [Benchmarks](#benchmarking-and-profiling)
- [Creating a new release](#creating-a-new-release)
- [Ecosystem CI](#ecosystem-ci)
- [Benchmarking and Profiling](#benchmarking-and-profiling)
- [CPython Benchmark](#cpython-benchmark)
- [Microbenchmarks](#microbenchmarks)
- [Benchmark-driven Development](#benchmark-driven-development)
- [PR Summary](#pr-summary)
- [Tips](#tips)
- [Profiling Projects](#profiling-projects)
- [Linux](#linux)
- [Mac](#mac)
- [`cargo dev`](#cargo-dev)
- [Subsystems](#subsystems)
- [Compilation Pipeline](#compilation-pipeline)
## The Basics
@@ -23,7 +36,10 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
community.
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
that are ready for contributions.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
@@ -34,6 +50,8 @@ As a concrete example: consider taking on one of the rules from the [`flake8-pyi
plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for
guidance.
If you have suggestions on how we might improve the contributing documentation, [let us know](https://github.com/astral-sh/ruff/discussions/5693)!
### Prerequisites
Ruff is written in Rust. You'll need to install the
@@ -92,48 +110,56 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
If you're working on a rule, this is the crate for you.
- `crates/ruff_benchmark`: binary crate for running micro-benchmarks.
- `crates/ruff_cache`: library crate for caching lint results.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`).
- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs.
- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate
representation.
`cargo dev generate-all`), see the [`cargo dev`](#cargo-dev) section below.
- `crates/ruff_diagnostics`: library crate for the rule-independent abstractions in the lint
diagnostics APIs.
- `crates/ruff_formatter`: library crate for language agnostic code formatting logic based on an
intermediate representation. The backend for `ruff_python_formatter`.
- `crates/ruff_index`: library crate inspired by `rustc_index`.
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic.
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities. Note
that the AST schema itself is defined in the
[rustpython-ast](https://github.com/astral-sh/RustPython-Parser) crate.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.
- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic,
including Ruff's semantic model.
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data.
including Ruff's semantic model. Used to resolve queries like "What import does this variable
refer to?"
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data, e.g.
the names of all built-in exceptions and which standard library types are immutable.
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
logic.
logic (indentation and newlines).
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff.
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the
[Ruff Playground](https://play.ruff.rs/).
### Example: Adding a new lint rule
At a high level, the steps involved in adding a new lint rule are as follows:
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention)
(e.g., `AssertFalse`, as in, "allow `assert False`").
(e.g., `AssertFalse`, as in, "allow `assert False`").
1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs`).
1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for
`#[violation]` to see examples.
`#[violation]` to see examples.
1. In that file, define a function that adds the violation to the diagnostic list as appropriate
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
an `ast::StmtAssert` node).
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
an `ast::StmtAssert` node).
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`).
@@ -166,13 +192,13 @@ suppression comment would be framed as "allow `assert False`".
As such, rule names should...
- Highlight the pattern that is being linted against, rather than the preferred alternative.
For example, `AssertFalse` guards against `assert False` statements.
For example, `AssertFalse` guards against `assert False` statements.
- _Not_ contain instructions on how to fix the violation, which instead belong in the rule
documentation and the `autofix_title`.
documentation and the `autofix_title`.
- _Not_ contain a redundant prefix, like `Disallow` or `Banned`, which are already implied by the
convention.
convention.
When re-implementing rules from other linters, we prioritize adhering to this convention over
preserving the original rule name.
@@ -187,25 +213,25 @@ Ruff's output for each fixture, which you can then commit alongside your changes
Once you've completed the code for the rule itself, you can define tests with the following steps:
1. Add a Python file to `crates/ruff/resources/test/fixtures/[linter]` that contains the code you
want to test. The file name should match the rule name (e.g., `E402.py`), and it should include
examples of both violations and non-violations.
want to test. The file name should match the rule name (e.g., `E402.py`), and it should include
examples of both violations and non-violations.
1. Run Ruff locally against your file and verify the output is as expected. Once you're satisfied
with the output (you see the violations you expect, and no others), proceed to the next step.
For example, if you're adding a new rule named `E402`, you would run:
with the output (you see the violations you expect, and no others), proceed to the next step.
For example, if you're adding a new rule named `E402`, you would run:
```shell
cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache
```
```shell
cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache
```
1. Add the test to the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. If you're contributing
a rule to a pre-existing set, you should be able to find a similar example to pattern-match
against. If you're adding a new linter, you'll need to create a new `mod.rs` file (see,
e.g., `crates/ruff/src/rules/flake8_bugbear/mod.rs`)
a rule to a pre-existing set, you should be able to find a similar example to pattern-match
against. If you're adding a new linter, you'll need to create a new `mod.rs` file (see,
e.g., `crates/ruff/src/rules/flake8_bugbear/mod.rs`)
1. Run `cargo test`. Your test will fail, but you'll be prompted to follow-up
with `cargo insta review`. Run `cargo insta review`, review and accept the generated snapshot,
then commit the snapshot file alongside the rest of your changes.
with `cargo insta review`. Run `cargo insta review`, review and accept the generated snapshot,
then commit the snapshot file alongside the rest of your changes.
1. Run `cargo test` again to ensure that your test passes.
@@ -243,25 +269,25 @@ To preview any changes to the documentation locally:
1. Install MkDocs and Material for MkDocs with:
```shell
pip install -r docs/requirements.txt
```
```shell
pip install -r docs/requirements.txt
```
1. Generate the MkDocs site with:
```shell
python scripts/generate_mkdocs.py
```
```shell
python scripts/generate_mkdocs.py
```
1. Run the development server with:
```shell
# For contributors.
mkdocs serve -f mkdocs.generated.yml
```shell
# For contributors.
mkdocs serve -f mkdocs.generated.yml
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
mkdocs serve -f mkdocs.insiders.yml
```
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
mkdocs serve -f mkdocs.insiders.yml
```
The documentation should then be available locally at
[http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
@@ -282,20 +308,19 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Create a PR with the version and `BREAKING_CHANGES.md` updated
1. Merge the PR
1. Run the release workflow with the version number (without starting `v`) as input. Make sure
main has your merged PR as last commit
main has your merged PR as last commit
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we havent tagged or
uploaded anything, you can restart after pushing a fix
1. Upload to pypi
1. Create and push the git tag (from pyproject.toml). We create the git tag only here
because we can't change it ([#4468](https://github.com/charliermarsh/ruff/issues/4468)), so
we want to make sure everything up to and including publishing to pypi worked.
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail without causing fallout, it is possible (if
inconvenient) to trigger the downstream jobs manually
1. Create release notes in GitHub UI and promote from draft to proper release(<https://github.com/charliermarsh/ruff/releases/new>)
1. Build all the assets. If this fails (even though we tested in step 4), we havent tagged or
uploaded anything, you can restart after pushing a fix.
1. Upload to PyPI.
1. Create and push the Git tag (as extracted from `pyproject.toml`). We create the Git tag only
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/charliermarsh/ruff/issues/4468)).
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed.
1. Create release notes in GitHub UI and promote from draft.
1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py)
1. If needed, update ruff-lsp and ruff-vscode
1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories.
## Ecosystem CI
@@ -394,6 +419,13 @@ Summary
159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython'
```
To benchmark a subset of rules, e.g. `LineTooLong` and `DocLineTooLong`:
```shell
cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e --select W505,E501"
```
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.
@@ -438,7 +470,7 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
Range (min … max): 29.813 s … 30.356 s 10 runs
```
## Microbenchmarks
### Microbenchmarks
The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files.
@@ -448,7 +480,7 @@ You can run the benchmarks with
cargo benchmark
```
### Benchmark driven Development
#### Benchmark-driven Development
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
@@ -463,7 +495,7 @@ cargo benchmark --save-baseline=main
cargo benchmark --baseline=main
```
### PR Summary
#### PR Summary
You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings.
This is useful to illustrate the improvements of a PR.
@@ -484,21 +516,21 @@ You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comp
cargo install critcmp
```
### Tips
#### Tips
- Use `cargo benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic`
to only run the pydantic tests.
to only run the pydantic tests.
- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance)
- Use `cargo benchmark --quick` to get faster results (more prone to noise)
## Profiling Projects
### Profiling Projects
You can either use the microbenchmarks from above or a project directory for benchmarking. There
are a lot of profiling tools out there,
[The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some
examples.
### Linux
#### Linux
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
@@ -531,7 +563,7 @@ An alternative is to convert the perf data to `flamegraph.svg` using
flamegraph --perfdata perf.data
```
### Mac
#### Mac
Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments):
@@ -546,7 +578,7 @@ cargo instruments -t time --bench linter --profile release-debug -p ruff_benchma
```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
for profiling the allocations.
for profiling the allocations.
- You may want to pass an additional filter to run a single test file
Otherwise, follow the instructions from the linux section.
@@ -557,10 +589,10 @@ Otherwise, follow the instructions from the linux section.
utils with it:
- `cargo dev print-ast <file>`: Print the AST of a python file using the
[RustPython parser](https://github.com/astral-sh/RustPython-Parser/tree/main/parser) that is
mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets
for start and stop of each node and also how the `:` token, the comment and whitespace are not
represented anymore:
[RustPython parser](https://github.com/astral-sh/RustPython-Parser/tree/main/parser) that is
mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets
for start and stop of each node and also how the `:` token, the comment and whitespace are not
represented anymore:
```text
[
@@ -590,7 +622,7 @@ utils with it:
```
- `cargo dev print-tokens <file>`: Print the tokens that the AST is built upon. Again for
`if True: pass # comment`:
`if True: pass # comment`:
```text
0 If 2
@@ -604,8 +636,8 @@ utils with it:
```
- `cargo dev print-cst <file>`: Print the CST of a python file using
[LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser
in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented:
[LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser
in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented:
```text
Module {
@@ -671,13 +703,54 @@ Module {
```
- `cargo dev generate-all`: Update `ruff.schema.json`, `docs/configuration.md` and `docs/rules`.
You can also set `RUFF_UPDATE_SCHEMA=1` to update `ruff.schema.json` during `cargo test`.
You can also set `RUFF_UPDATE_SCHEMA=1` to update `ruff.schema.json` during `cargo test`.
- `cargo dev generate-cli-help`, `cargo dev generate-docs` and `cargo dev generate-json-schema`:
Update just `docs/configuration.md`, `docs/rules` and `ruff.schema.json` respectively.
Update just `docs/configuration.md`, `docs/rules` and `ruff.schema.json` respectively.
- `cargo dev generate-options`: Generate a markdown-compatible table of all `pyproject.toml`
options. Used for <https://beta.ruff.rs/docs/settings/>
options. Used for <https://beta.ruff.rs/docs/settings/>
- `cargo dev generate-rules-table`: Generate a markdown-compatible table of all rules. Used for <https://beta.ruff.rs/docs/rules/>
- `cargo dev round-trip <python file or jupyter notebook>`: Read a Python file or Jupyter Notebook,
parse it, serialize the parsed representation and write it back. Used to check how good our
representation is so that fixes don't rewrite irrelevant parts of a file.
parse it, serialize the parsed representation and write it back. Used to check how good our
representation is so that fixes don't rewrite irrelevant parts of a file.
- `cargo dev format_dev`: See ruff_python_formatter README.md
## Subsystems
### Compilation Pipeline
If we view Ruff as a compiler, in which the inputs are paths to Python files and the outputs are
diagnostics, then our current compilation pipeline proceeds as follows:
1. **File discovery**: Given paths like `foo/`, locate all Python files in any specified subdirectories, taking into account our hierarchical settings system and any `exclude` options.
1. **Package resolution**: Determine the “package root” for every file by traversing over its parent directories and looking for `__init__.py` files.
1. **Cache initialization**: For every “package root”, initialize an empty cache.
1. **Analysis**: For every file, in parallel:
1. **Cache read**: If the file is cached (i.e., its modification timestamp hasn't changed since it was last analyzed), short-circuit, and return the cached diagnostics.
1. **Tokenization**: Run the lexer over the file to generate a token stream.
1. **Indexing**: Extract metadata from the token stream, such as: comment ranges, `# noqa` locations, `# isort: off` locations, “doc lines”, etc.
1. **Token-based rule evaluation**: Run any lint rules that are based on the contents of the token stream (e.g., commented-out code).
1. **Filesystem-based rule evaluation**: Run any lint rules that are based on the contents of the filesystem (e.g., lack of `__init__.py` file in a package).
1. **Logical line-based rule evaluation**: Run any lint rules that are based on logical lines (e.g., stylistic rules).
1. **Parsing**: Run the parser over the token stream to produce an AST. (This consumes the token stream, so anything that relies on the token stream needs to happen before parsing.)
1. **AST-based rule evaluation**: Run any lint rules that are based on the AST. This includes the vast majority of lint rules. As part of this step, we also build the semantic model for the current file as we traverse over the AST. Some lint rules are evaluated eagerly, as we iterate over the AST, while others are evaluated in a deferred manner (e.g., unused imports, since we cant determine whether an import is unused until weve finished analyzing the entire file), after weve finished the initial traversal.
1. **Import-based rule evaluation**: Run any lint rules that are based on the modules imports (e.g., import sorting). These could, in theory, be included in the AST-based rule evaluation phase — theyre just separated for simplicity.
1. **Physical line-based rule evaluation**: Run any lint rules that are based on physical lines (e.g., line-length).
1. **Suppression enforcement**: Remove any violations that are suppressed via `# noqa` directives or `per-file-ignores`.
1. **Cache write**: Write the generated diagnostics to the package cache using the file as a key.
1. **Reporting**: Print diagnostics in the specified format (text, JSON, etc.), to the specified output channel (stdout, a file, etc.).

284
Cargo.lock generated
View File

@@ -14,15 +14,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@@ -120,9 +111,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.71"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]]
name = "argfile"
@@ -135,9 +126,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.11"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151"
checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
dependencies = [
"anstyle",
"bstr",
@@ -279,9 +270,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.11"
version = "4.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
checksum = "98330784c494e49850cb23b8e2afcca13587d2500b2e3f1f78ae20248059c9be"
dependencies = [
"clap_builder",
"clap_derive",
@@ -290,9 +281,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.11"
version = "4.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
checksum = "e182eb5f2562a67dda37e2c57af64d720a9e010c5e860ed87c056586aeafa52e"
dependencies = [
"anstream",
"anstyle",
@@ -343,14 +334,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.3.2"
version = "4.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -534,21 +525,11 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "darling"
version = "0.20.1"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
dependencies = [
"darling_core",
"darling_macro",
@@ -556,27 +537,27 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.1"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
name = "darling_macro"
version = "0.20.1"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -646,9 +627,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
[[package]]
name = "dyn-clone"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
[[package]]
name = "either"
@@ -677,9 +658,9 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
@@ -804,11 +785,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
dependencies = [
"aho-corasick 0.7.20",
"aho-corasick",
"bstr",
"fnv",
"log",
@@ -963,6 +944,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "indoc"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
[[package]]
name = "inotify"
version = "0.9.6"
@@ -985,9 +972,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.30.0"
version = "1.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3"
checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a"
dependencies = [
"console",
"globset",
@@ -1033,12 +1020,12 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.3",
"rustix 0.38.4",
"windows-sys 0.48.0",
]
@@ -1053,9 +1040,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
@@ -1380,20 +1367,11 @@ dependencies = [
"memchr",
]
[[package]]
name = "output_vt100"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
dependencies = [
"winapi",
]
[[package]]
name = "paste"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "path-absolutize"
@@ -1520,7 +1498,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1579,9 +1557,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.3.3"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6"
[[package]]
name = "predicates"
@@ -1613,13 +1591,11 @@ dependencies = [
[[package]]
name = "pretty_assertions"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"ctor",
"diff",
"output_vt100",
"yansi",
]
@@ -1649,9 +1625,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.63"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
@@ -1671,12 +1647,12 @@ dependencies = [
[[package]]
name = "quick-junit"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd"
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
dependencies = [
"chrono",
"indexmap 1.9.3",
"indexmap 2.0.0",
"nextest-workspace-hack",
"quick-xml",
"thiserror",
@@ -1685,18 +1661,18 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.26.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.29"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [
"proc-macro2",
]
@@ -1769,11 +1745,11 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.0"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
dependencies = [
"aho-corasick 1.0.2",
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
@@ -1781,20 +1757,20 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.0"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
dependencies = [
"aho-corasick 1.0.2",
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "result-like"
@@ -1899,6 +1875,7 @@ dependencies = [
"typed-arena",
"unicode-width",
"unicode_names2",
"wsl",
]
[[package]]
@@ -1986,6 +1963,7 @@ dependencies = [
"clap",
"ignore",
"indicatif",
"indoc",
"itertools",
"libcst",
"log",
@@ -2003,11 +1981,13 @@ dependencies = [
"rustpython-format",
"rustpython-parser",
"schemars",
"serde",
"serde_json",
"similar",
"strum",
"strum_macros",
"tempfile",
"toml",
]
[[package]]
@@ -2051,7 +2031,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_textwrap",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2154,7 +2134,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"schemars",
"serde",
@@ -2219,9 +2199,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.3"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
dependencies = [
"bitflags 2.3.3",
"errno",
@@ -2232,13 +2212,13 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.2"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
dependencies = [
"log",
"ring",
"rustls-webpki",
"rustls-webpki 0.101.1",
"sct",
]
@@ -2252,10 +2232,20 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.101.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"is-macro",
"num-bigint",
@@ -2266,7 +2256,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"bitflags 2.3.3",
"itertools",
@@ -2278,7 +2268,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2290,7 +2280,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"anyhow",
"is-macro",
@@ -2313,7 +2303,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=126652b684910c29a7bcc32293d4ca0f81454e34#126652b684910c29a7bcc32293d4ca0f81454e34"
dependencies = [
"is-macro",
"memchr",
@@ -2322,15 +2312,15 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "same-file"
@@ -2373,9 +2363,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
@@ -2389,15 +2379,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.166"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
dependencies = [
"serde_derive",
]
@@ -2415,13 +2405,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.166"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2437,9 +2427,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.100"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [
"itoa",
"ryu",
@@ -2457,9 +2447,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.0.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
dependencies = [
"base64",
"chrono",
@@ -2468,19 +2458,19 @@ dependencies = [
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.22",
"time 0.3.23",
]
[[package]]
name = "serde_with_macros"
version = "3.0.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2506,9 +2496,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "spin"
@@ -2563,9 +2553,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.23"
version = "2.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
dependencies = [
"proc-macro2",
"quote",
@@ -2675,7 +2665,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2721,9 +2711,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
dependencies = [
"itoa",
"serde",
@@ -2739,9 +2729,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
dependencies = [
"time-core",
]
@@ -2782,9 +2772,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [
"serde",
"serde_spanned",
@@ -2803,9 +2793,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.11"
version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.0.0",
"serde",
@@ -2835,7 +2825,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2925,9 +2915,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-normalization"
@@ -2969,7 +2959,7 @@ dependencies = [
"log",
"once_cell",
"rustls",
"rustls-webpki",
"rustls-webpki 0.100.1",
"url",
"webpki-roots",
]
@@ -2994,9 +2984,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
[[package]]
name = "version_check"
@@ -3056,7 +3046,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-shared",
]
@@ -3090,7 +3080,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3141,7 +3131,7 @@ version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki",
"rustls-webpki 0.100.1",
]
[[package]]
@@ -3338,13 +3328,19 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.7"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
dependencies = [
"memchr",
]
[[package]]
name = "wsl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4"
[[package]]
name = "yaml-rust"
version = "0.4.5"

View File

@@ -47,18 +47,19 @@ syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
thiserror = { version = "1.0.43" }
toml = { version = "0.7.2" }
wsl = { version = "0.1.0" }
# v1.0.1
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false }
# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
# Current tag: v0.0.7
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] }
# Note: As of tag v0.0.8 we are cherry-picking commits instead of rebasing so the tag is not necessary
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "126652b684910c29a7bcc32293d4ca0f81454e34" , default-features = false, features = ["full-lexer", "num-bigint"] }
[profile.release]
lto = "fat"

View File

@@ -32,10 +32,10 @@ An extremely fast Python linter, written in Rust.
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party [editor integrations](https://beta.ruff.rs/docs/editor-integrations/) for
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
@@ -364,8 +364,8 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Great Expectations](https://github.com/great-expectations/great_expectations)
- [HTTPX](https://github.com/encode/httpx)
- Hugging Face ([Transformers](https://github.com/huggingface/transformers),
[Datasets](https://github.com/huggingface/datasets),
[Diffusers](https://github.com/huggingface/diffusers))
[Datasets](https://github.com/huggingface/datasets),
[Diffusers](https://github.com/huggingface/diffusers))
- [Hatch](https://github.com/pypa/hatch)
- [Home Assistant](https://github.com/home-assistant/core)
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
@@ -377,8 +377,8 @@ Ruff is used by a number of major open-source projects and companies, including:
- [MegaLinter](https://github.com/oxsecurity/megalinter)
- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk))
- Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel),
[ONNX Runtime](https://github.com/microsoft/onnxruntime),
[LightGBM](https://github.com/microsoft/LightGBM))
[ONNX Runtime](https://github.com/microsoft/onnxruntime),
[LightGBM](https://github.com/microsoft/LightGBM))
- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk))
- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev))
- [Mypy](https://github.com/python/mypy)

View File

@@ -82,12 +82,12 @@ flake8-to-ruff path/to/.flake8 --plugin flake8-builtins --plugin flake8-quotes
## Limitations
1. Ruff only supports a subset of the Flake configuration options. `flake8-to-ruff` will warn on and
ignore unsupported options in the `.flake8` file (or equivalent). (Similarly, Ruff has a few
configuration options that don't exist in Flake8.)
ignore unsupported options in the `.flake8` file (or equivalent). (Similarly, Ruff has a few
configuration options that don't exist in Flake8.)
1. Ruff will omit any rule codes that are unimplemented or unsupported by Ruff, including rule
codes from unsupported plugins. (See the
[documentation](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) for the complete
list of supported plugins.)
codes from unsupported plugins. (See the
[documentation](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) for the complete
list of supported plugins.)
## License

View File

@@ -78,6 +78,7 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { version = "0.1.10" }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
wsl = { version = "0.1.0" }
[dev-dependencies]
insta = { workspace = true }

View File

@@ -1,4 +1,4 @@
from typing import Any, Type
from typing import Annotated, Any, Optional, Type, Union
from typing_extensions import override
# Error
@@ -95,27 +95,27 @@ class Foo:
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
pass
# ANN401
# OK
@override
def foo(self: "Foo", a: Any, *params: str, **options: str) -> int:
pass
# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: str, **options: str) -> Any:
pass
# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int:
pass
# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: Any, **options: str) -> int:
pass
# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
pass
@@ -137,3 +137,18 @@ class Foo:
# OK
def f(*args: *tuple[int]) -> None: ...
def f(a: object) -> None: ...
def f(a: str | bytes) -> None: ...
def f(a: Union[str, bytes]) -> None: ...
def f(a: Optional[str]) -> None: ...
def f(a: Annotated[str, ...]) -> None: ...
def f(a: "Union[str, bytes]") -> None: ...
def f(a: int + int) -> None: ...
# ANN401
def f(a: Any | int) -> None: ...
def f(a: int | Any) -> None: ...
def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...

View File

@@ -177,6 +177,9 @@ def str_okay(value=str("foo")):
def bool_okay(value=bool("bar")):
pass
# Allow immutable bytes() value
def bytes_okay(value=bytes(1)):
pass
# Allow immutable int() value
def int_okay(value=int("12")):

View File

@@ -14,9 +14,10 @@ except AssertionError:
except Exception as err:
assert err
raise Exception("No cause here...")
except BaseException as base_err:
# Might use this instead of bare raise with the `.with_traceback()` method
raise base_err
except BaseException as err:
raise err
except BaseException as err:
raise some_other_err
finally:
raise Exception("Nothing to chain from, so no warning here")

View File

@@ -12,7 +12,8 @@ set(reversed(x))
sorted(list(x))
sorted(tuple(x))
sorted(sorted(x))
sorted(sorted(x, key=lambda y: y))
sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
sorted(sorted(x, reverse=True), reverse=True)
sorted(reversed(x))
sorted(list(x), key=lambda y: y)
tuple(
@@ -21,3 +22,9 @@ tuple(
"o"]
)
)
# Nested sorts with differing keyword arguments. Not flagged.
sorted(sorted(x, key=lambda y: y))
sorted(sorted(x, key=lambda y: y), key=lambda x: x)
sorted(sorted(x), reverse=True)
sorted(sorted(x, reverse=False), reverse=True)

View File

@@ -59,7 +59,6 @@ field18: typing.Union[
],
] # Error, newline and comment will not be emitted in message
# Should emit in cases with `typing.Union` instead of `|`
field19: typing.Union[int, int] # Error
@@ -71,3 +70,7 @@ field21: typing.Union[int, int | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]

View File

@@ -0,0 +1,75 @@
import builtins
import types
import typing
from collections.abc import Awaitable
from types import TracebackType
from typing import Any, Type
import _typeshed
import typing_extensions
from _typeshed import Unused
class GoodOne:
def __exit__(self, *args: object) -> None: ...
async def __aexit__(self, *args) -> str: ...
class GoodTwo:
def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ...
async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ...
class GoodThree:
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...
class GoodFour:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ...
class GoodFive:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ...
class GoodSix:
def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ...
async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ...
class GoodSeven:
def __exit__(self, *args: Unused) -> bool: ...
async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ...
class GoodEight:
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class GoodNine:
def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class GoodTen:
def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class BadOne:
def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
async def __aexit__(self) -> None: ... # PYI036: Missing args
class BadTwo:
def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ...# PYI036: Extra arg must have default
class BadThree:
def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
class BadFour:
def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
class BadFive:
def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
class BadSix:
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default

View File

@@ -0,0 +1,75 @@
import builtins
import types
import typing
from collections.abc import Awaitable
from types import TracebackType
from typing import Any, Type
import _typeshed
import typing_extensions
from _typeshed import Unused
class GoodOne:
def __exit__(self, *args: object) -> None: ...
async def __aexit__(self, *args) -> str: ...
class GoodTwo:
def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ...
async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ...
class GoodThree:
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...
class GoodFour:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ...
class GoodFive:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ...
class GoodSix:
def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ...
async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ...
class GoodSeven:
def __exit__(self, *args: Unused) -> bool: ...
async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ...
class GoodEight:
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class GoodNine:
def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class GoodTen:
def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ...
async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
class BadOne:
def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
async def __aexit__(self) -> None: ... # PYI036: Missing args
class BadTwo:
def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default
class BadThree:
def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
class BadFour:
def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
class BadFive:
def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
class BadSix:
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default

View File

@@ -0,0 +1,47 @@
from typing import (
Union,
)
from typing_extensions import (
TypeAlias,
)
TA0: TypeAlias = int
TA1: TypeAlias = int | float | bool
TA2: TypeAlias = Union[int, float, bool]
def good1(arg: int) -> int | bool:
...
def good2(arg: int, arg2: int | bool) -> None:
...
def f0(arg1: float | int) -> None:
...
def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None:
...
def f2(arg1: int, /, arg2: int | int | float) -> None:
...
def f3(arg1: int, *args: Union[int | int | float]) -> None:
...
async def f4(**kwargs: int | int | float) -> None:
...
class Foo:
def good(self, arg: int) -> None:
...
def bad(self, arg: int | float | complex) -> None:
...

View File

@@ -0,0 +1,39 @@
from typing import (
Union,
)
from typing_extensions import (
TypeAlias,
)
# Type aliases not flagged
TA0: TypeAlias = int
TA1: TypeAlias = int | float | bool
TA2: TypeAlias = Union[int, float, bool]
def good1(arg: int) -> int | bool: ...
def good2(arg: int, arg2: int | bool) -> None: ...
def f0(arg1: float | int) -> None: ... # PYI041
def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041
def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041
def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041
async def f4(**kwargs: int | int | float) -> None: ... # PYI041
class Foo:
def good(self, arg: int) -> None: ...
def bad(self, arg: int | float | complex) -> None: ... # PYI041

View File

@@ -4,3 +4,10 @@ class Bad(str): # SLOT000
class Good(str): # Ok
__slots__ = ["foo"]
from enum import Enum
class Fine(str, Enum): # Ok
__slots__ = ["foo"]

View File

@@ -0,0 +1,27 @@
import pandas as pd
data = pd.Series(range(1000))
# PD101
data.nunique() <= 1
data.nunique(dropna=True) <= 1
data.nunique(dropna=False) <= 1
data.nunique() == 1
data.nunique(dropna=True) == 1
data.nunique(dropna=False) == 1
data.nunique() != 1
data.nunique(dropna=True) != 1
data.nunique(dropna=False) != 1
data.nunique() > 1
data.dropna().nunique() == 1
data[data.notnull()].nunique() == 1
# No violation of this rule
data.nunique() == 0 # empty
data.nunique() >= 1 # not-empty
data.nunique() < 1 # empty
data.nunique() == 2 # not constant
data.unique() == 1 # not `nunique`
{"hello": "world"}.nunique() == 1 # no pd.Series

View File

@@ -0,0 +1,20 @@
import pandas as pd
# Errors.
df = pd.read_table("data.csv", sep=",")
df = pd.read_table("data.csv", sep=",", header=0)
filename = "data.csv"
df = pd.read_table(filename, sep=",")
df = pd.read_table(filename, sep=",", header=0)
# Non-errors.
df = pd.read_csv("data.csv")
df = pd.read_table("data.tsv")
df = pd.read_table("data.tsv", sep="\t")
df = pd.read_table("data.tsv", sep=",,")
df = pd.read_table("data.tsv", sep=", ")
df = pd.read_table("data.tsv", sep=" ,")
df = pd.read_table("data.tsv", sep=" , ")
not_pd.read_table("data.csv", sep=",")
data = read_table("data.csv", sep=",")
data = read_table

View File

@@ -1,71 +1,101 @@
some_dict = {"a": 12, "b": 32, "c": 44}
for _, value in some_dict.items(): # PERF102
print(value)
def f():
for _, value in some_dict.items(): # PERF102
print(value)
for key, _ in some_dict.items(): # PERF102
print(key)
def f():
for key, _ in some_dict.items(): # PERF102
print(key)
for weird_arg_name, _ in some_dict.items(): # PERF102
print(weird_arg_name)
def f():
for weird_arg_name, _ in some_dict.items(): # PERF102
print(weird_arg_name)
for name, (_, _) in some_dict.items(): # PERF102
pass
def f():
for name, (_, _) in some_dict.items(): # PERF102
print(name)
for name, (value1, _) in some_dict.items(): # OK
pass
def f():
for name, (value1, _) in some_dict.items(): # OK
print(name, value1)
for (key1, _), (_, _) in some_dict.items(): # PERF102
pass
def f():
for (key1, _), (_, _) in some_dict.items(): # PERF102
print(key1)
for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
pass
def f():
for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
print(value)
for (_, key2), (value1, _) in some_dict.items(): # OK
pass
def f():
for (_, key2), (value1, _) in some_dict.items(): # OK
print(key2, value1)
for ((_, key2), (value1, _)) in some_dict.items(): # OK
pass
def f():
for ((_, key2), (value1, _)) in some_dict.items(): # OK
print(key2, value1)
for ((_, key2), (_, _)) in some_dict.items(): # PERF102
pass
def f():
for ((_, key2), (_, _)) in some_dict.items(): # PERF102
print(key2)
for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK
pass
def f():
for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK
print(variants, r_language)
for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK
pass
def f():
for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK
print(variants, r_language)
for key, value in some_dict.items(): # OK
print(key, value)
def f():
for key, value in some_dict.items(): # OK
print(key, value)
for _, value in some_dict.items(12): # OK
print(value)
def f():
for _, value in some_dict.items(12): # OK
print(value)
for key in some_dict.keys(): # OK
print(key)
def f():
for key in some_dict.keys(): # OK
print(key)
for value in some_dict.values(): # OK
print(value)
def f():
for value in some_dict.values(): # OK
print(value)
for name, (_, _) in (some_function()).items(): # PERF102
pass
def f():
for name, (_, _) in (some_function()).items(): # PERF102
print(name)
for name, (_, _) in (some_function().some_attribute).items(): # PERF102
pass
def f():
for name, (_, _) in (some_function().some_attribute).items(): # PERF102
print(name)
def f():
for name, unused_value in some_dict.items(): # PERF102
print(name)
def f():
for unused_name, value in some_dict.items(): # PERF102
print(value)

View File

@@ -36,3 +36,4 @@ if (True) == TrueElement or x == TrueElement:
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar
assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None)

View File

@@ -36,3 +36,4 @@ if (True) == TrueElement or x == TrueElement:
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar
assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None)

View File

@@ -80,3 +80,8 @@ def multiple_assignment():
global CONSTANT # [global-statement]
CONSTANT = 1
CONSTANT = 2
def no_assignment():
"""Shouldn't warn"""
global CONSTANT

View File

@@ -0,0 +1,34 @@
# Errors.
foo == "a" or foo == "b"
foo != "a" and foo != "b"
foo == "a" or foo == "b" or foo == "c"
foo != "a" and foo != "b" and foo != "c"
foo == a or foo == "b" or foo == 3 # Mixed types.
# False negatives (the current implementation doesn't support Yoda conditions).
"a" == foo or "b" == foo or "c" == foo
"a" != foo and "b" != foo and "c" != foo
"a" == foo or foo == "b" or "c" == foo
# OK
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
foo != "a" or foo != "b" or foo != "c" # `or` mixed with `!=`.
foo == a or foo == b() or foo == c # Call expression.
foo != a or foo() != b or foo != c # Call expression.
foo in {"a", "b", "c"} # Uses membership test already.
foo not in {"a", "b", "c"} # Uses membership test already.
foo == "a" # Single comparison.
foo != "a" # Single comparison.

View File

@@ -27,6 +27,14 @@ def f(x: typing.Union[(str, int), float]) -> None:
...
def f(x: typing.Union[(int,)]) -> None:
...
def f(x: typing.Union[()]) -> None:
...
def f(x: "Union[str, int, Union[float, bytes]]") -> None:
...

View File

@@ -4,23 +4,9 @@ import typing
# with complex annotations
MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
# with default values as list
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str), ("c", list[bool])],
defaults=["foo", [True]],
)
# with namespace
MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
# too many default values (OK)
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str)],
defaults=[1, "bar", "baz"],
)
# invalid identifiers (OK)
MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])
@@ -29,3 +15,10 @@ MyType = typing.NamedTuple("MyType")
# empty fields
MyType = typing.NamedTuple("MyType", [])
# keywords
MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)

View File

@@ -1,3 +1,5 @@
"""A mirror of UP037_1.py, with `from __future__ import annotations`."""
from __future__ import annotations
from typing import (

View File

@@ -0,0 +1,108 @@
"""A mirror of UP037_0.py, without `from __future__ import annotations`."""
from typing import (
Annotated,
Callable,
List,
Literal,
NamedTuple,
Tuple,
TypeVar,
TypedDict,
cast,
)
from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg
def foo(var: "MyClass") -> "MyClass":
x: "MyClass"
def foo(*, inplace: "bool"):
pass
def foo(*args: "str", **kwargs: "int"):
pass
x: Tuple["MyClass"]
x: Callable[["MyClass"], None]
class Foo(NamedTuple):
x: "MyClass"
class D(TypedDict):
E: TypedDict("E", foo="int", total=False)
class D(TypedDict):
E: TypedDict("E", {"foo": "int"})
x: Annotated["str", "metadata"]
x: Arg("str", "name")
x: DefaultArg("str", "name")
x: NamedArg("str", "name")
x: DefaultNamedArg("str", "name")
x: DefaultNamedArg("str", name="name")
x: VarArg("str")
x: List[List[List["MyClass"]]]
x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
x: NamedTuple(typename="X", fields=[("foo", "int")])
X: MyCallable("X")
# OK
class D(TypedDict):
E: TypedDict("E")
x: Annotated[()]
x: DefaultNamedArg(name="name", quox="str")
x: DefaultNamedArg(name="name")
x: NamedTuple("X", [("foo",), ("bar",)])
x: NamedTuple("X", ["foo", "bar"])
x: NamedTuple()
x: Literal["foo", "bar"]
x = cast(x, "str")
def foo(x, *args, **kwargs):
...
def foo(*, inplace):
...
x: Annotated[1:2] = ...
x = TypeVar("x", "str", "int")
x = cast("str", x)
X = List["MyClass"]

View File

@@ -48,6 +48,10 @@ def f(arg: typing.Optional[int] = None):
# Union
def f(arg: Union[None] = None):
pass
def f(arg: Union[None, int] = None):
pass
@@ -68,6 +72,10 @@ def f(arg: Union = None): # RUF013
pass
def f(arg: Union[int] = None): # RUF013
pass
def f(arg: Union[int, str] = None): # RUF013
pass
@@ -106,10 +114,18 @@ def f(arg: None = None):
pass
def f(arg: Literal[None] = None):
pass
def f(arg: Literal[1, 2, None, 3] = None):
pass
def f(arg: Literal[1] = None): # RUF013
pass
def f(arg: Literal[1, "foo"] = None): # RUF013
pass

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::UniversalNewlines;
use crate::comments::shebang::ShebangDirective;
use crate::registry::Rule;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
use crate::rules::flake8_executable::helpers::ShebangDirective;
use crate::rules::flake8_executable::rules::{
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
};
@@ -115,7 +115,6 @@ pub(crate) fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
} else {
}
}
}

View File

@@ -209,6 +209,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0915") => (RuleGroup::Unspecified, rules::pylint::rules::TooManyStatements),
(Pylint, "R1701") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1711") => (RuleGroup::Unspecified, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedEqualityComparisonTarget),
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
@@ -603,6 +604,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(PandasVet, "012") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotReadTable),
(PandasVet, "013") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotStack),
(PandasVet, "015") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfPdMerge),
(PandasVet, "101") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck),
(PandasVet, "901") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasDfVariableName),
// flake8-errmsg
@@ -637,6 +639,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub),
(Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType),
(Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub),
(Flake8Pyi, "036") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadExitAnnotation),
(Flake8Pyi, "041") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::RedundantNumericUnion),
(Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias),
(Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias),
(Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub),

View File

@@ -0,0 +1 @@
pub(crate) mod shebang;

View File

@@ -0,0 +1,67 @@
use ruff_python_whitespace::{is_python_whitespace, Cursor};
use ruff_text_size::{TextLen, TextSize};
/// A shebang directive (e.g., `#!/usr/bin/env python3`).
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ShebangDirective<'a> {
/// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the
/// line.
pub(crate) offset: TextSize,
/// The contents of the directive (e.g., `"/usr/bin/env python3"`).
pub(crate) contents: &'a str,
}
impl<'a> ShebangDirective<'a> {
/// Parse a shebang directive from a line, or return `None` if the line does not contain a
/// shebang directive.
pub(crate) fn try_extract(line: &'a str) -> Option<Self> {
let mut cursor = Cursor::new(line);
// Trim whitespace.
cursor.eat_while(is_python_whitespace);
// Trim the `#!` prefix.
if !cursor.eat_char('#') {
return None;
}
if !cursor.eat_char('!') {
return None;
}
Some(Self {
offset: line.text_len() - cursor.text_len(),
contents: cursor.chars().as_str(),
})
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use super::ShebangDirective;
#[test]
fn shebang_non_match() {
let source = "not a match";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_end_of_line() {
let source = "print('test') #!/usr/bin/python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_match() {
let source = "#!/usr/bin/env python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_leading_space() {
let source = " #!/usr/bin/env python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
}

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/flake8_executable/helpers.rs
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
None

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/flake8_executable/helpers.rs
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/flake8_executable/helpers.rs
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/flake8_executable/helpers.rs
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
None

View File

@@ -14,6 +14,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
mod autofix;
mod checkers;
mod codes;
mod comments;
mod cst;
pub mod directives;
mod doc_lines;

View File

@@ -51,11 +51,9 @@ pub(crate) fn variable_name_task_id(
value: &Expr,
) -> Option<Diagnostic> {
// If we have more than one target, we can't do anything.
if targets.len() != 1 {
let [target] = targets else {
return None;
}
let target = &targets[0];
};
let Expr::Name(ast::ExprName { id, .. }) = target else {
return None;
};

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{ArgWithDefault, Expr, Ranged, Stmt};
use rustpython_parser::ast::{self, ArgWithDefault, Constant, Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -6,12 +6,14 @@ use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
use ruff_python_semantic::{Definition, Member, MemberKind};
use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::ruff::typing::type_hint_resolves_to_any;
use super::super::fixes;
use super::super::helpers::match_function_def;
@@ -432,20 +434,46 @@ fn is_none_returning(body: &[Stmt]) -> bool {
/// ANN401
fn check_dynamically_typed<F>(
checker: &Checker,
annotation: &Expr,
func: F,
diagnostics: &mut Vec<Diagnostic>,
is_overridden: bool,
semantic: &SemanticModel,
) where
F: FnOnce() -> String,
{
if !is_overridden && semantic.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
};
if let Expr::Constant(ast::ExprConstant {
range,
value: Constant::Str(string),
..
}) = annotation
{
// Quoted annotations
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) {
if type_hint_resolves_to_any(
&parsed_annotation,
checker.semantic(),
checker.locator,
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
}
}
} else {
if type_hint_resolves_to_any(
annotation,
checker.semantic(),
checker.locator,
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
}
}
}
/// Generate flake8-annotation checks for a given `Definition`.
@@ -500,13 +528,12 @@ pub(crate) fn definition(
// ANN401 for dynamically typed arguments
if let Some(annotation) = &def.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
check_dynamically_typed(
checker,
annotation,
|| def.arg.to_string(),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
}
} else {
@@ -530,15 +557,9 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
check_dynamically_typed(
expr,
|| format!("*{name}"),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics);
}
}
} else {
@@ -562,14 +583,13 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
check_dynamically_typed(
checker,
expr,
|| format!("**{name}"),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
}
}
@@ -629,14 +649,8 @@ pub(crate) fn definition(
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker.enabled(Rule::AnyType) {
check_dynamically_typed(
expr,
|| name.to_string(),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
if checker.enabled(Rule::AnyType) && !is_overridden {
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
}
} else if !(
// Allow omission of return annotation if the function only returns `None`

View File

@@ -186,4 +186,60 @@ annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in meth
135 | pass
|
annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
148 | # ANN401
149 | def f(a: Any | int) -> None: ...
| ^^^^^^^^^ ANN401
150 | def f(a: int | Any) -> None: ...
151 | def f(a: Union[str, bytes, Any]) -> None: ...
|
annotation_presence.py:150:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
148 | # ANN401
149 | def f(a: Any | int) -> None: ...
150 | def f(a: int | Any) -> None: ...
| ^^^^^^^^^ ANN401
151 | def f(a: Union[str, bytes, Any]) -> None: ...
152 | def f(a: Optional[Any]) -> None: ...
|
annotation_presence.py:151:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
149 | def f(a: Any | int) -> None: ...
150 | def f(a: int | Any) -> None: ...
151 | def f(a: Union[str, bytes, Any]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^ ANN401
152 | def f(a: Optional[Any]) -> None: ...
153 | def f(a: Annotated[Any, ...]) -> None: ...
|
annotation_presence.py:152:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
150 | def f(a: int | Any) -> None: ...
151 | def f(a: Union[str, bytes, Any]) -> None: ...
152 | def f(a: Optional[Any]) -> None: ...
| ^^^^^^^^^^^^^ ANN401
153 | def f(a: Annotated[Any, ...]) -> None: ...
154 | def f(a: "Union[str, bytes, Any]") -> None: ...
|
annotation_presence.py:153:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
151 | def f(a: Union[str, bytes, Any]) -> None: ...
152 | def f(a: Optional[Any]) -> None: ...
153 | def f(a: Annotated[Any, ...]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^ ANN401
154 | def f(a: "Union[str, bytes, Any]") -> None: ...
|
annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
152 | def f(a: Optional[Any]) -> None: ...
153 | def f(a: Annotated[Any, ...]) -> None: ...
154 | def f(a: "Union[str, bytes, Any]") -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|

View File

@@ -5,6 +5,7 @@ use rustpython_parser::ast::{self, Expr, Operator, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -52,7 +53,7 @@ fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Option<String> {
fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool {
match expr {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
@@ -60,45 +61,37 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
op: Operator::Add | Operator::Mod,
..
}) => {
let Some(parent) = checker.semantic().expr_parent() else {
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
}
return None;
};
// Only evaluate the full BinOp, not the nested components.
let Expr::BinOp(_) = parent else {
if model
.expr_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
return true;
}
return None;
};
None
}
false
}
Expr::Call(ast::ExprCall { func, .. }) => {
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else {
return None;
return false;
};
// "select * from table where val = {}".format(...)
if attr == "format" && string_literal(value).is_some() {
return Some(checker.generator().expr(expr));
};
None
attr == "format" && string_literal(value).is_some()
}
// f"select * from table where val = {val}"
Expr::JoinedStr(_) => Some(checker.generator().expr(expr)),
_ => None,
Expr::JoinedStr(_) => true,
_ => false,
}
}
/// S608
pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
match unparse_string_format_expression(checker, expr) {
Some(string) if matches_sql_statement(&string) => {
if matches_string_format_expression(expr, checker.semantic()) {
if matches_sql_statement(&checker.generator().expr(expr)) {
checker
.diagnostics
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
}
_ => (),
}
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -30,12 +29,7 @@ impl Violation for Jinja2AutoescapeFalse {
}
/// S701
pub(crate) fn jinja2_autoescape_false(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if checker
.semantic()
.resolve_call_path(func)
@@ -43,10 +37,13 @@ pub(crate) fn jinja2_autoescape_false(
matches!(call_path.as_slice(), ["jinja2", "Environment"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(autoescape_arg) = call_args.keyword_argument("autoescape") {
match autoescape_arg {
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "autoescape")
}) {
match &keyword.value {
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),
..
@@ -56,14 +53,14 @@ pub(crate) fn jinja2_autoescape_false(
if id != "select_autoescape" {
checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
autoescape_arg.range(),
keyword.range(),
));
}
}
}
_ => checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
autoescape_arg.range(),
keyword.range(),
)),
}
} else {

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -20,7 +19,6 @@ impl Violation for LoggingConfigInsecureListen {
pub(crate) fn logging_config_insecure_listen(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker
@@ -30,12 +28,17 @@ pub(crate) fn logging_config_insecure_listen(
matches!(call_path.as_slice(), ["logging", "config", "listen"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if call_args.keyword_argument("verify").is_none() {
checker
.diagnostics
.push(Diagnostic::new(LoggingConfigInsecureListen, func.range()));
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(LoggingConfigInsecureListen, func.range()));
}
}

View File

@@ -2,10 +2,34 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs};
use ruff_python_ast::helpers::is_const_false;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for HTTPS requests that disable SSL certificate checks.
///
/// ## Why is this bad?
/// If SSL certificates are not verified, an attacker could perform a "man in
/// the middle" attack by intercepting and modifying traffic between the client
/// and server.
///
/// ## Example
/// ```python
/// import requests
///
/// requests.get("https://www.example.com", verify=False)
/// ```
///
/// Use instead:
/// ```python
/// import requests
///
/// requests.get("https://www.example.com") # By default, `verify=True`.
/// ```
///
/// ## References
/// - [Common Weakness Enumeration: CWE-295](https://cwe.mitre.org/data/definitions/295.html)
#[violation]
pub struct RequestWithNoCertValidation {
string: String,
@@ -25,7 +49,6 @@ impl Violation for RequestWithNoCertValidation {
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if let Some(target) = checker
@@ -40,14 +63,18 @@ pub(crate) fn request_with_no_cert_validation(
_ => None,
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(verify_arg) = call_args.keyword_argument("verify") {
if is_const_false(verify_arg) {
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
if is_const_false(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithNoCertValidation {
string: target.to_string(),
},
verify_arg.range(),
keyword.range(),
));
}
}

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs};
use ruff_python_ast::helpers::is_const_none;
use crate::checkers::ast::Checker;
@@ -49,12 +49,7 @@ impl Violation for RequestWithoutTimeout {
}
/// S113
pub(crate) fn request_without_timeout(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if checker
.semantic()
.resolve_call_path(func)
@@ -68,12 +63,16 @@ pub(crate) fn request_without_timeout(
)
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout) = call_args.keyword_argument("timeout") {
if is_const_none(timeout) {
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "timeout")
}) {
if is_const_none(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
timeout.range(),
keyword.range(),
));
}
} else {

View File

@@ -3,10 +3,34 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of SNMPv1 or SNMPv2.
///
/// ## Why is this bad?
/// The SNMPv1 and SNMPv2 protocols are considered insecure as they do
/// not support encryption. Instead, prefer SNMPv3, which supports
/// encryption.
///
/// ## Example
/// ```python
/// from pysnmp.hlapi import CommunityData
///
/// CommunityData("public", mpModel=0)
/// ```
///
/// Use instead:
/// ```python
/// from pysnmp.hlapi import CommunityData
///
/// CommunityData("public", mpModel=2)
/// ```
///
/// ## References
/// - [Cybersecurity and Infrastructure Security Agency (CISA): Alert TA17-156A](https://www.cisa.gov/news-events/alerts/2017/06/05/reducing-risk-snmp-abuse)
/// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html)
#[violation]
pub struct SnmpInsecureVersion;
@@ -18,12 +42,7 @@ impl Violation for SnmpInsecureVersion {
}
/// S508
pub(crate) fn snmp_insecure_version(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if checker
.semantic()
.resolve_call_path(func)
@@ -31,17 +50,21 @@ pub(crate) fn snmp_insecure_version(
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(mp_model_arg) = call_args.keyword_argument("mpModel") {
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "mpModel")
}) {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = &mp_model_arg
}) = &keyword.value
{
if value.is_zero() || value.is_one() {
checker
.diagnostics
.push(Diagnostic::new(SnmpInsecureVersion, mp_model_arg.range()));
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
}
}
}

View File

@@ -6,6 +6,29 @@ use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the SNMPv3 protocol without encryption.
///
/// ## Why is this bad?
/// Unencrypted SNMPv3 communication can be intercepted and read by
/// unauthorized parties. Instead, enable encryption when using SNMPv3.
///
/// ## Example
/// ```python
/// from pysnmp.hlapi import UsmUserData
///
/// UsmUserData("user")
/// ```
///
/// Use instead:
/// ```python
/// from pysnmp.hlapi import UsmUserData
///
/// UsmUserData("user", "authkey", "privkey")
/// ```
///
/// ## References
/// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html)
#[violation]
pub struct SnmpWeakCryptography;

View File

@@ -55,16 +55,14 @@ pub(crate) fn try_except_continue(
checker: &mut Checker,
except_handler: &ExceptHandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if body.len() == 1
&& body[0].is_continue_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(TryExceptContinue, except_handler.range()));
if matches!(body, [Stmt::Continue(_)]) {
if check_typed_exception || is_untyped_exception(type_, checker.semantic()) {
checker
.diagnostics
.push(Diagnostic::new(TryExceptContinue, except_handler.range()));
}
}
}

View File

@@ -51,16 +51,14 @@ pub(crate) fn try_except_pass(
checker: &mut Checker,
except_handler: &ExceptHandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if body.len() == 1
&& body[0].is_pass_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(TryExceptPass, except_handler.range()));
if matches!(body, [Stmt::Pass(_)]) {
if check_typed_exception || is_untyped_exception(type_, checker.semantic()) {
checker
.diagnostics
.push(Diagnostic::new(TryExceptPass, except_handler.range()));
}
}
}

View File

@@ -6,6 +6,35 @@ use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the `yaml.load` function.
///
/// ## Why is this bad?
/// Running the `yaml.load` function over untrusted YAML files is insecure, as
/// `yaml.load` allows for the creation of arbitrary Python objects, which can
/// then be used to execute arbitrary code.
///
/// Instead, consider using `yaml.safe_load`, which allows for the creation of
/// simple Python objects like integers and lists, but prohibits the creation of
/// more complex objects like functions and classes.
///
/// ## Example
/// ```python
/// import yaml
///
/// yaml.load(untrusted_yaml)
/// ```
///
/// Use instead:
/// ```python
/// import yaml
///
/// yaml.safe_load(untrusted_yaml)
/// ```
///
/// ## References
/// - [PyYAML documentation: Loading YAML](https://pyyaml.org/wiki/PyYAMLDocumentation)
/// - [Common Weakness Enumeration: CWE-20](https://cwe.mitre.org/data/definitions/20.html)
#[violation]
pub struct UnsafeYAMLLoad {
pub loader: Option<String>,

View File

@@ -11,11 +11,11 @@ S113.py:3:1: S113 Probable use of requests call without timeout
5 | requests.get('https://gmail.com', timeout=5)
|
S113.py:4:43: S113 Probable use of requests call with timeout set to `None`
S113.py:4:35: S113 Probable use of requests call with timeout set to `None`
|
3 | requests.get('https://gmail.com')
4 | requests.get('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
|
@@ -30,12 +30,12 @@ S113.py:6:1: S113 Probable use of requests call without timeout
8 | requests.post('https://gmail.com', timeout=5)
|
S113.py:7:44: S113 Probable use of requests call with timeout set to `None`
S113.py:7:36: S113 Probable use of requests call with timeout set to `None`
|
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
7 | requests.post('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
|
@@ -50,12 +50,12 @@ S113.py:9:1: S113 Probable use of requests call without timeout
11 | requests.put('https://gmail.com', timeout=5)
|
S113.py:10:43: S113 Probable use of requests call with timeout set to `None`
S113.py:10:35: S113 Probable use of requests call with timeout set to `None`
|
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
10 | requests.put('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
|
@@ -70,12 +70,12 @@ S113.py:12:1: S113 Probable use of requests call without timeout
14 | requests.delete('https://gmail.com', timeout=5)
|
S113.py:13:46: S113 Probable use of requests call with timeout set to `None`
S113.py:13:38: S113 Probable use of requests call with timeout set to `None`
|
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
13 | requests.delete('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
|
@@ -90,12 +90,12 @@ S113.py:15:1: S113 Probable use of requests call without timeout
17 | requests.patch('https://gmail.com', timeout=5)
|
S113.py:16:45: S113 Probable use of requests call with timeout set to `None`
S113.py:16:37: S113 Probable use of requests call with timeout set to `None`
|
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
16 | requests.patch('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
|
@@ -110,12 +110,12 @@ S113.py:18:1: S113 Probable use of requests call without timeout
20 | requests.options('https://gmail.com', timeout=5)
|
S113.py:19:47: S113 Probable use of requests call with timeout set to `None`
S113.py:19:39: S113 Probable use of requests call with timeout set to `None`
|
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
19 | requests.options('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
|
@@ -130,12 +130,12 @@ S113.py:21:1: S113 Probable use of requests call without timeout
23 | requests.head('https://gmail.com', timeout=5)
|
S113.py:22:44: S113 Probable use of requests call with timeout set to `None`
S113.py:22:36: S113 Probable use of requests call with timeout set to `None`
|
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
22 | requests.head('https://gmail.com', timeout=None)
| ^^^^ S113
| ^^^^^^^^^^^^ S113
23 | requests.head('https://gmail.com', timeout=5)
|

View File

@@ -1,180 +1,180 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S501.py:5:54: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:5:47: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
4 | requests.get('https://gmail.com', timeout=30, verify=True)
5 | requests.get('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
6 | requests.post('https://gmail.com', timeout=30, verify=True)
7 | requests.post('https://gmail.com', timeout=30, verify=False)
|
S501.py:7:55: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:7:48: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
5 | requests.get('https://gmail.com', timeout=30, verify=False)
6 | requests.post('https://gmail.com', timeout=30, verify=True)
7 | requests.post('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
8 | requests.put('https://gmail.com', timeout=30, verify=True)
9 | requests.put('https://gmail.com', timeout=30, verify=False)
|
S501.py:9:54: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:9:47: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
7 | requests.post('https://gmail.com', timeout=30, verify=False)
8 | requests.put('https://gmail.com', timeout=30, verify=True)
9 | requests.put('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
10 | requests.delete('https://gmail.com', timeout=30, verify=True)
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
|
S501.py:11:57: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:11:50: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
9 | requests.put('https://gmail.com', timeout=30, verify=False)
10 | requests.delete('https://gmail.com', timeout=30, verify=True)
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
12 | requests.patch('https://gmail.com', timeout=30, verify=True)
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
|
S501.py:13:56: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:13:49: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
11 | requests.delete('https://gmail.com', timeout=30, verify=False)
12 | requests.patch('https://gmail.com', timeout=30, verify=True)
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
14 | requests.options('https://gmail.com', timeout=30, verify=True)
15 | requests.options('https://gmail.com', timeout=30, verify=False)
|
S501.py:15:58: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:15:51: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
13 | requests.patch('https://gmail.com', timeout=30, verify=False)
14 | requests.options('https://gmail.com', timeout=30, verify=True)
15 | requests.options('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
16 | requests.head('https://gmail.com', timeout=30, verify=True)
17 | requests.head('https://gmail.com', timeout=30, verify=False)
|
S501.py:17:55: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
S501.py:17:48: S501 Probable use of `requests` call with `verify=False` disabling SSL certificate checks
|
15 | requests.options('https://gmail.com', timeout=30, verify=False)
16 | requests.head('https://gmail.com', timeout=30, verify=True)
17 | requests.head('https://gmail.com', timeout=30, verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
18 |
19 | httpx.request('GET', 'https://gmail.com', verify=True)
|
S501.py:20:50: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:20:43: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
19 | httpx.request('GET', 'https://gmail.com', verify=True)
20 | httpx.request('GET', 'https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
21 | httpx.get('https://gmail.com', verify=True)
22 | httpx.get('https://gmail.com', verify=False)
|
S501.py:22:39: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:22:32: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
20 | httpx.request('GET', 'https://gmail.com', verify=False)
21 | httpx.get('https://gmail.com', verify=True)
22 | httpx.get('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
23 | httpx.options('https://gmail.com', verify=True)
24 | httpx.options('https://gmail.com', verify=False)
|
S501.py:24:43: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:24:36: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
22 | httpx.get('https://gmail.com', verify=False)
23 | httpx.options('https://gmail.com', verify=True)
24 | httpx.options('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
25 | httpx.head('https://gmail.com', verify=True)
26 | httpx.head('https://gmail.com', verify=False)
|
S501.py:26:40: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:26:33: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
24 | httpx.options('https://gmail.com', verify=False)
25 | httpx.head('https://gmail.com', verify=True)
26 | httpx.head('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
27 | httpx.post('https://gmail.com', verify=True)
28 | httpx.post('https://gmail.com', verify=False)
|
S501.py:28:40: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:28:33: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
26 | httpx.head('https://gmail.com', verify=False)
27 | httpx.post('https://gmail.com', verify=True)
28 | httpx.post('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
29 | httpx.put('https://gmail.com', verify=True)
30 | httpx.put('https://gmail.com', verify=False)
|
S501.py:30:39: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:30:32: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
28 | httpx.post('https://gmail.com', verify=False)
29 | httpx.put('https://gmail.com', verify=True)
30 | httpx.put('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
31 | httpx.patch('https://gmail.com', verify=True)
32 | httpx.patch('https://gmail.com', verify=False)
|
S501.py:32:41: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:32:34: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
30 | httpx.put('https://gmail.com', verify=False)
31 | httpx.patch('https://gmail.com', verify=True)
32 | httpx.patch('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
33 | httpx.delete('https://gmail.com', verify=True)
34 | httpx.delete('https://gmail.com', verify=False)
|
S501.py:34:42: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:34:35: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
32 | httpx.patch('https://gmail.com', verify=False)
33 | httpx.delete('https://gmail.com', verify=True)
34 | httpx.delete('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
35 | httpx.stream('https://gmail.com', verify=True)
36 | httpx.stream('https://gmail.com', verify=False)
|
S501.py:36:42: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:36:35: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
34 | httpx.delete('https://gmail.com', verify=False)
35 | httpx.stream('https://gmail.com', verify=True)
36 | httpx.stream('https://gmail.com', verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
37 | httpx.Client()
38 | httpx.Client(verify=False)
|
S501.py:38:21: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:38:14: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
36 | httpx.stream('https://gmail.com', verify=False)
37 | httpx.Client()
38 | httpx.Client(verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
39 | httpx.AsyncClient()
40 | httpx.AsyncClient(verify=False)
|
S501.py:40:26: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
S501.py:40:19: S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate checks
|
38 | httpx.Client(verify=False)
39 | httpx.AsyncClient()
40 | httpx.AsyncClient(verify=False)
| ^^^^^ S501
| ^^^^^^^^^^^^ S501
|

View File

@@ -1,20 +1,20 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S508.py:3:33: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
S508.py:3:25: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^ S508
| ^^^^^^^^^ S508
4 | CommunityData("public", mpModel=1) # S508
|
S508.py:4:33: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
S508.py:4:25: S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^ S508
| ^^^^^^^^^ S508
5 |
6 | CommunityData("public", mpModel=2) # OK
|

View File

@@ -1,32 +1,32 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S701.py:9:68: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:9:57: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
7 | templateEnv = jinja2.Environment(autoescape=True,
8 | loader=templateLoader )
9 | Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
| ^^^^^^^^^ S701
| ^^^^^^^^^^^^^^^^^^^^ S701
10 | templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
11 | Environment(loader=templateLoader,
|
S701.py:10:45: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:10:34: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
8 | loader=templateLoader )
9 | Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
10 | templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
| ^^^^^ S701
| ^^^^^^^^^^^^^^^^ S701
11 | Environment(loader=templateLoader,
12 | load=templateLoader,
|
S701.py:13:24: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:13:13: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
11 | Environment(loader=templateLoader,
12 | load=templateLoader,
13 | autoescape=False) # S701
| ^^^^^ S701
| ^^^^^^^^^^^^^^^^ S701
14 |
15 | Environment(loader=templateLoader, # S701
|
@@ -40,12 +40,12 @@ S701.py:15:1: S701 By default, jinja2 sets `autoescape` to `False`. Consider usi
16 | load=templateLoader)
|
S701.py:29:47: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
S701.py:29:36: S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
|
27 | def fake_func():
28 | return 'foobar'
29 | Environment(loader=templateLoader, autoescape=fake_func()) # S701
| ^^^^^^^^^^^ S701
| ^^^^^^^^^^^^^^^^^^^^^^ S701
|

View File

@@ -47,12 +47,12 @@ impl Violation for AssignmentToOsEnviron {
format!("Assigning to `os.environ` doesn't clear the environment")
}
}
/// B003
pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) {
if targets.len() != 1 {
let [target] = targets else {
return;
}
let target = &targets[0];
};
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = target else {
return;
};

View File

@@ -1,5 +1,4 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged};
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -47,15 +46,6 @@ impl AlwaysAutofixableViolation for GetAttrWithConstant {
"Replace `getattr` with attribute access".to_string()
}
}
fn attribute(value: &Expr, attr: &str) -> Expr {
ast::ExprAttribute {
value: Box::new(value.clone()),
attr: Identifier::new(attr.to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
}
.into()
}
/// B009
pub(crate) fn getattr_with_constant(
@@ -83,14 +73,14 @@ pub(crate) fn getattr_with_constant(
if !is_identifier(value) {
return;
}
if is_mangled_private(value.as_str()) {
if is_mangled_private(value) {
return;
}
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().expr(&attribute(obj, value)),
format!("{}.{}", checker.locator.slice(obj.range()), value),
expr.range(),
)));
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
@@ -38,12 +37,7 @@ impl Violation for NoExplicitStacklevel {
}
/// B028
pub(crate) fn no_explicit_stacklevel(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if !checker
.semantic()
.resolve_call_path(func)
@@ -54,10 +48,12 @@ pub(crate) fn no_explicit_stacklevel(
return;
}
if SimpleCallArgs::new(args, keywords)
.keyword_argument("stacklevel")
.is_some()
{
if keywords.iter().any(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "stacklevel")
}) {
return;
}

View File

@@ -1,10 +1,10 @@
use rustpython_parser::ast::{self, Expr, Stmt};
use rustpython_parser::ast;
use rustpython_parser::ast::Stmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::RaiseStatementVisitor;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_stdlib::str::is_cased_lowercase;
use crate::checkers::ast::Checker;
@@ -60,7 +60,11 @@ impl Violation for RaiseWithoutFromInsideExcept {
}
/// B904
pub(crate) fn raise_without_from_inside_except(checker: &mut Checker, body: &[Stmt]) {
pub(crate) fn raise_without_from_inside_except(
checker: &mut Checker,
name: Option<&str>,
body: &[Stmt],
) {
let raises = {
let mut visitor = RaiseStatementVisitor::default();
visitor.visit_body(body);
@@ -70,14 +74,28 @@ pub(crate) fn raise_without_from_inside_except(checker: &mut Checker, body: &[St
for (range, exc, cause) in raises {
if cause.is_none() {
if let Some(exc) = exc {
match exc {
Expr::Name(ast::ExprName { id, .. }) if is_cased_lowercase(id) => {}
_ => {
checker
.diagnostics
.push(Diagnostic::new(RaiseWithoutFromInsideExcept, range));
// If the raised object is bound to the same name, it's a re-raise, which is
// allowed (but may be flagged by other diagnostics).
//
// For example:
// ```python
// try:
// ...
// except ValueError as exc:
// raise exc
// ```
if let Some(name) = name {
if exc
.as_name_expr()
.map_or(false, |ast::ExprName { id, .. }| name == id)
{
continue;
}
}
checker
.diagnostics
.push(Diagnostic::new(RaiseWithoutFromInsideExcept, range));
}
}
}

View File

@@ -55,14 +55,11 @@ pub(crate) fn strip_with_multi_characters(
if !matches!(attr.as_str(), "strip" | "lstrip" | "rstrip") {
return;
}
if args.len() != 1 {
return;
}
let Expr::Constant(ast::ExprConstant {
let [Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
}) = &args[0]
})] = args
else {
return;
};

View File

@@ -1,9 +1,10 @@
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `hasattr` to test if an object is callable (e.g.,
@@ -35,13 +36,19 @@ use crate::checkers::ast::Checker;
pub struct UnreliableCallableCheck;
impl Violation for UnreliableCallableCheck {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
"Using `hasattr(x, \"__call__\")` to test if x is callable is unreliable. Use \
`callable(x)` for consistent results."
)
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `callable()`"))
}
}
/// B004
@@ -54,23 +61,33 @@ pub(crate) fn unreliable_callable_check(
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if id != "getattr" && id != "hasattr" {
if !matches!(id.as_str(), "hasattr" | "getattr") {
return;
}
if args.len() < 2 {
let [obj, attr, ..] = args else {
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
value: Constant::Str(string),
..
}) = &args[1]
}) = attr
else {
return;
};
if s != "__call__" {
if string != "__call__" {
return;
}
checker
.diagnostics
.push(Diagnostic::new(UnreliableCallableCheck, expr.range()));
let mut diagnostic = Diagnostic::new(UnreliableCallableCheck, expr.range());
if checker.patch(diagnostic.kind.rule()) {
if id == "hasattr" {
if checker.semantic().is_builtin("callable") {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("callable({})", checker.locator.slice(obj.range())),
expr.range(),
)));
}
}
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -154,20 +154,23 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
},
expr.range(),
);
if let Some(rename) = rename {
if certainty.into() && checker.patch(diagnostic.kind.rule()) {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
rename,
expr.range(),
)));
if checker.patch(diagnostic.kind.rule()) {
if let Some(rename) = rename {
if certainty.into() {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.filter(|binding| binding.range.start() >= expr.range().start())
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
rename,
expr.range(),
)));
}
}
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B004.py:3:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
B004.py:3:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
1 | def this_is_a_bug():
2 | o = object()
@@ -10,8 +10,18 @@ B004.py:3:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unr
4 | print("Ooh, callable! Or is it?")
5 | if getattr(o, "__call__", False):
|
= help: Replace with `callable()`
B004.py:5:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
Fix
1 1 | def this_is_a_bug():
2 2 | o = object()
3 |- if hasattr(o, "__call__"):
3 |+ if callable(o):
4 4 | print("Ooh, callable! Or is it?")
5 5 | if getattr(o, "__call__", False):
6 6 | print("Ooh, callable! Or is it?")
B004.py:5:8: B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
3 | if hasattr(o, "__call__"):
4 | print("Ooh, callable! Or is it?")
@@ -19,5 +29,6 @@ B004.py:5:8: B004 Using `hasattr(x, '__call__')` to test if x is callable is unr
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B004
6 | print("Ooh, callable! Or is it?")
|
= help: Replace with `callable()`

View File

@@ -72,42 +72,42 @@ B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaul
101 | ...
|
B006_B008.py:218:20: B006 Do not use mutable data structures for argument defaults
B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaults
|
216 | # B006 and B008
217 | # We should handle arbitrary nesting of these B008.
218 | def nested_combo(a=[float(3), dt.datetime.now()]):
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
219 | pass
222 | pass
|
B006_B008.py:251:27: B006 Do not use mutable data structures for argument defaults
B006_B008.py:254:27: B006 Do not use mutable data structures for argument defaults
|
250 | def mutable_annotations(
251 | a: list[int] | None = [],
253 | def mutable_annotations(
254 | a: list[int] | None = [],
| ^^ B006
252 | b: Optional[Dict[int, int]] = {},
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
B006_B008.py:252:35: B006 Do not use mutable data structures for argument defaults
B006_B008.py:255:35: B006 Do not use mutable data structures for argument defaults
|
250 | def mutable_annotations(
251 | a: list[int] | None = [],
252 | b: Optional[Dict[int, int]] = {},
253 | def mutable_annotations(
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
| ^^ B006
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
254 | ):
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
257 | ):
|
B006_B008.py:253:62: B006 Do not use mutable data structures for argument defaults
B006_B008.py:256:62: B006 Do not use mutable data structures for argument defaults
|
251 | a: list[int] | None = [],
252 | b: Optional[Dict[int, int]] = {},
253 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
254 | ):
255 | pass
257 | ):
258 | pass
|

View File

@@ -46,38 +46,38 @@ B006_B008.py:120:30: B008 Do not perform function call in argument defaults
121 | ...
|
B006_B008.py:218:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:221:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
216 | # B006 and B008
217 | # We should handle arbitrary nesting of these B008.
218 | def nested_combo(a=[float(3), dt.datetime.now()]):
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
219 | pass
222 | pass
|
B006_B008.py:224:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:227:22: B008 Do not perform function call `map` in argument defaults
|
222 | # Don't flag nested B006 since we can't guarantee that
223 | # it isn't made mutable by the outer operation.
224 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
225 | # Don't flag nested B006 since we can't guarantee that
226 | # it isn't made mutable by the outer operation.
227 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
225 | pass
228 | pass
|
B006_B008.py:229:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:232:19: B008 Do not perform function call `random.randint` in argument defaults
|
228 | # B008-ception.
229 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
230 | pass
233 | pass
|
B006_B008.py:229:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:232:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
228 | # B008-ception.
229 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
230 | pass
233 | pass
|

View File

@@ -27,33 +27,43 @@ B904.py:16:5: B904 Within an `except` clause, raise exceptions with `raise ... f
15 | assert err
16 | raise Exception("No cause here...")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
17 | except BaseException as base_err:
18 | # Might use this instead of bare raise with the `.with_traceback()` method
17 | except BaseException as err:
18 | raise err
|
B904.py:62:9: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
B904.py:20:5: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
60 | except Exception as e:
61 | if ...:
62 | raise RuntimeError("boom!")
18 | raise err
19 | except BaseException as err:
20 | raise some_other_err
| ^^^^^^^^^^^^^^^^^^^^ B904
21 | finally:
22 | raise Exception("Nothing to chain from, so no warning here")
|
B904.py:63:9: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
61 | except Exception as e:
62 | if ...:
63 | raise RuntimeError("boom!")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
63 | else:
64 | raise RuntimeError("bang!")
64 | else:
65 | raise RuntimeError("bang!")
|
B904.py:64:9: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
B904.py:65:9: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
62 | raise RuntimeError("boom!")
63 | else:
64 | raise RuntimeError("bang!")
63 | raise RuntimeError("boom!")
64 | else:
65 | raise RuntimeError("bang!")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|
B904.py:72:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
B904.py:73:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
70 | match 0:
71 | case 0:
72 | raise RuntimeError("boom!")
71 | match 0:
72 | case 0:
73 | raise RuntimeError("boom!")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904
|

View File

@@ -1,44 +1,5 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt};
use ruff_python_ast::identifier::{Identifier, TryIdentifier};
use ruff_python_stdlib::builtins::BUILTINS;
use ruff_python_stdlib::builtins::is_builtin;
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum AnyShadowing<'a> {
Expression(&'a Expr),
Statement(&'a Stmt),
ExceptHandler(&'a ExceptHandler),
}
impl Identifier for AnyShadowing<'_> {
fn identifier(&self) -> TextRange {
match self {
AnyShadowing::Expression(expr) => expr.range(),
AnyShadowing::Statement(stmt) => stmt.identifier(),
AnyShadowing::ExceptHandler(handler) => handler.try_identifier().unwrap(),
}
}
}
impl<'a> From<&'a Stmt> for AnyShadowing<'a> {
fn from(value: &'a Stmt) -> Self {
AnyShadowing::Statement(value)
}
}
impl<'a> From<&'a Expr> for AnyShadowing<'a> {
fn from(value: &'a Expr) -> Self {
AnyShadowing::Expression(value)
}
}
impl<'a> From<&'a ExceptHandler> for AnyShadowing<'a> {
fn from(value: &'a ExceptHandler) -> Self {
AnyShadowing::ExceptHandler(value)
}
is_builtin(name) && ignorelist.iter().all(|ignore| ignore != name)
}

View File

@@ -1,12 +1,12 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use rustpython_parser::ast;
use crate::checkers::ast::Checker;
use super::super::helpers::{shadows_builtin, AnyShadowing};
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
/// Checks for any class attributes that use the same name as a builtin.
@@ -67,7 +67,7 @@ pub(crate) fn builtin_attribute_shadowing(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
name: &str,
shadowing: AnyShadowing,
range: TextRange,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
@@ -84,7 +84,7 @@ pub(crate) fn builtin_attribute_shadowing(
BuiltinAttributeShadowing {
name: name.to_string(),
},
shadowing.identifier(),
range,
));
}
}

View File

@@ -1,11 +1,11 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use crate::checkers::ast::Checker;
use super::super::helpers::{shadows_builtin, AnyShadowing};
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
/// Checks for variable (and function) assignments that use the same name
@@ -59,17 +59,13 @@ impl Violation for BuiltinVariableShadowing {
}
/// A001
pub(crate) fn builtin_variable_shadowing(
checker: &mut Checker,
name: &str,
shadowing: AnyShadowing,
) {
pub(crate) fn builtin_variable_shadowing(checker: &mut Checker, name: &str, range: TextRange) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
checker.diagnostics.push(Diagnostic::new(
BuiltinVariableShadowing {
name: name.to_string(),
},
shadowing.identifier(),
range,
));
}
}

View File

@@ -1,28 +1,28 @@
---
source: crates/ruff/src/rules/flake8_builtins/mod.rs
---
A001.py:1:1: A001 Variable `sum` is shadowing a Python builtin
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^^^^^^^^^^^^^^^^ A001
| ^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:1: A001 Variable `int` is shadowing a Python builtin
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
| ^^^ A001
3 | from directory import new as dir
|
A001.py:3:1: A001 Variable `dir` is shadowing a Python builtin
A001.py:3:30: A001 Variable `dir` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
3 | from directory import new as dir
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
| ^^^ A001
4 |
5 | print = 1
|

View File

@@ -1,19 +1,19 @@
---
source: crates/ruff/src/rules/flake8_builtins/mod.rs
---
A001.py:1:1: A001 Variable `sum` is shadowing a Python builtin
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^^^^^^^^^^^^^^^^ A001
| ^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:1: A001 Variable `int` is shadowing a Python builtin
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A001
| ^^^ A001
3 | from directory import new as dir
|

View File

@@ -14,16 +14,16 @@ pub(super) fn exactly_one_argument_with_matching_function<'a>(
args: &'a [Expr],
keywords: &[Keyword],
) -> Option<&'a Expr> {
if !keywords.is_empty() {
let [arg] = args else {
return None;
}
if args.len() != 1 {
};
if !keywords.is_empty() {
return None;
}
if expr_name(func)? != name {
return None;
}
Some(&args[0])
Some(arg)
}
pub(super) fn first_argument_with_matching_function<'a>(

View File

@@ -82,10 +82,9 @@ pub(crate) fn unnecessary_dict_comprehension(
value: &Expr,
generators: &[Comprehension],
) {
if generators.len() != 1 {
let [generator] = generators else {
return;
}
let generator = &generators[0];
};
if !generator.ifs.is_empty() || generator.is_async {
return;
}
@@ -123,10 +122,9 @@ pub(crate) fn unnecessary_list_set_comprehension(
elt: &Expr,
generators: &[Comprehension],
) {
if generators.len() != 1 {
let [generator] = generators else {
return;
}
let generator = &generators[0];
};
if !generator.ifs.is_empty() || generator.is_async {
return;
}

View File

@@ -1,7 +1,8 @@
use rustpython_parser::ast::{self, Expr, Ranged};
use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableKeyword;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -69,6 +70,7 @@ pub(crate) fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
args: &[Expr],
outer_kw: &[Keyword],
) {
let Some(outer) = helpers::expr_name(func) else {
return;
@@ -84,7 +86,12 @@ pub(crate) fn unnecessary_double_cast_or_process(
let Some(arg) = args.first() else {
return;
};
let Expr::Call(ast::ExprCall { func, .. }) = arg else {
let Expr::Call(ast::ExprCall {
func,
keywords: inner_kw,
..
}) = arg
else {
return;
};
let Some(inner) = helpers::expr_name(func) else {
@@ -94,6 +101,21 @@ pub(crate) fn unnecessary_double_cast_or_process(
return;
}
// Avoid collapsing nested `sorted` calls with non-identical keyword arguments
// (i.e., `key`, `reverse`).
if inner == "sorted" && outer == "sorted" {
if inner_kw.len() != outer_kw.len() {
return;
}
if !inner_kw.iter().all(|inner| {
outer_kw
.iter()
.any(|outer| ComparableKeyword::from(inner) == ComparableKeyword::from(outer))
}) {
return;
}
}
// Ex) set(tuple(...))
// Ex) list(tuple(...))
// Ex) set(set(...))

View File

@@ -226,7 +226,7 @@ C414.py:12:1: C414 [*] Unnecessary `list` call within `sorted()`
12 |+sorted(x)
13 13 | sorted(tuple(x))
14 14 | sorted(sorted(x))
15 15 | sorted(sorted(x, key=lambda y: y))
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
C414.py:13:1: C414 [*] Unnecessary `tuple` call within `sorted()`
|
@@ -235,7 +235,7 @@ C414.py:13:1: C414 [*] Unnecessary `tuple` call within `sorted()`
13 | sorted(tuple(x))
| ^^^^^^^^^^^^^^^^ C414
14 | sorted(sorted(x))
15 | sorted(sorted(x, key=lambda y: y))
15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
|
= help: Remove the inner `tuple` call
@@ -246,8 +246,8 @@ C414.py:13:1: C414 [*] Unnecessary `tuple` call within `sorted()`
13 |-sorted(tuple(x))
13 |+sorted(x)
14 14 | sorted(sorted(x))
15 15 | sorted(sorted(x, key=lambda y: y))
16 16 | sorted(reversed(x))
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 16 | sorted(sorted(x, reverse=True), reverse=True)
C414.py:14:1: C414 [*] Unnecessary `sorted` call within `sorted()`
|
@@ -255,8 +255,8 @@ C414.py:14:1: C414 [*] Unnecessary `sorted` call within `sorted()`
13 | sorted(tuple(x))
14 | sorted(sorted(x))
| ^^^^^^^^^^^^^^^^^ C414
15 | sorted(sorted(x, key=lambda y: y))
16 | sorted(reversed(x))
15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 | sorted(sorted(x, reverse=True), reverse=True)
|
= help: Remove the inner `sorted` call
@@ -266,18 +266,18 @@ C414.py:14:1: C414 [*] Unnecessary `sorted` call within `sorted()`
13 13 | sorted(tuple(x))
14 |-sorted(sorted(x))
14 |+sorted(x)
15 15 | sorted(sorted(x, key=lambda y: y))
16 16 | sorted(reversed(x))
17 17 | sorted(list(x), key=lambda y: y)
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 16 | sorted(sorted(x, reverse=True), reverse=True)
17 17 | sorted(reversed(x))
C414.py:15:1: C414 [*] Unnecessary `sorted` call within `sorted()`
|
13 | sorted(tuple(x))
14 | sorted(sorted(x))
15 | sorted(sorted(x, key=lambda y: y))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C414
16 | sorted(reversed(x))
17 | sorted(list(x), key=lambda y: y)
15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C414
16 | sorted(sorted(x, reverse=True), reverse=True)
17 | sorted(reversed(x))
|
= help: Remove the inner `sorted` call
@@ -285,77 +285,103 @@ C414.py:15:1: C414 [*] Unnecessary `sorted` call within `sorted()`
12 12 | sorted(list(x))
13 13 | sorted(tuple(x))
14 14 | sorted(sorted(x))
15 |-sorted(sorted(x, key=lambda y: y))
15 |+sorted(x, )
16 16 | sorted(reversed(x))
17 17 | sorted(list(x), key=lambda y: y)
18 18 | tuple(
15 |-sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
15 |+sorted(x, reverse=False, key=foo)
16 16 | sorted(sorted(x, reverse=True), reverse=True)
17 17 | sorted(reversed(x))
18 18 | sorted(list(x), key=lambda y: y)
C414.py:16:1: C414 [*] Unnecessary `reversed` call within `sorted()`
C414.py:16:1: C414 [*] Unnecessary `sorted` call within `sorted()`
|
14 | sorted(sorted(x))
15 | sorted(sorted(x, key=lambda y: y))
16 | sorted(reversed(x))
| ^^^^^^^^^^^^^^^^^^^ C414
17 | sorted(list(x), key=lambda y: y)
18 | tuple(
15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 | sorted(sorted(x, reverse=True), reverse=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C414
17 | sorted(reversed(x))
18 | sorted(list(x), key=lambda y: y)
|
= help: Remove the inner `reversed` call
= help: Remove the inner `sorted` call
Suggested fix
13 13 | sorted(tuple(x))
14 14 | sorted(sorted(x))
15 15 | sorted(sorted(x, key=lambda y: y))
16 |-sorted(reversed(x))
16 |+sorted(x)
17 17 | sorted(list(x), key=lambda y: y)
18 18 | tuple(
19 19 | list(
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 |-sorted(sorted(x, reverse=True), reverse=True)
16 |+sorted(x, reverse=True)
17 17 | sorted(reversed(x))
18 18 | sorted(list(x), key=lambda y: y)
19 19 | tuple(
C414.py:17:1: C414 [*] Unnecessary `list` call within `sorted()`
C414.py:17:1: C414 [*] Unnecessary `reversed` call within `sorted()`
|
15 | sorted(sorted(x, key=lambda y: y))
16 | sorted(reversed(x))
17 | sorted(list(x), key=lambda y: y)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C414
18 | tuple(
19 | list(
15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 | sorted(sorted(x, reverse=True), reverse=True)
17 | sorted(reversed(x))
| ^^^^^^^^^^^^^^^^^^^ C414
18 | sorted(list(x), key=lambda y: y)
19 | tuple(
|
= help: Remove the inner `list` call
= help: Remove the inner `reversed` call
Suggested fix
14 14 | sorted(sorted(x))
15 15 | sorted(sorted(x, key=lambda y: y))
16 16 | sorted(reversed(x))
17 |-sorted(list(x), key=lambda y: y)
17 |+sorted(x, key=lambda y: y)
18 18 | tuple(
19 19 | list(
20 20 | [x, 3, "hell"\
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 16 | sorted(sorted(x, reverse=True), reverse=True)
17 |-sorted(reversed(x))
17 |+sorted(x)
18 18 | sorted(list(x), key=lambda y: y)
19 19 | tuple(
20 20 | list(
C414.py:18:1: C414 [*] Unnecessary `list` call within `tuple()`
C414.py:18:1: C414 [*] Unnecessary `list` call within `sorted()`
|
16 | sorted(reversed(x))
17 | sorted(list(x), key=lambda y: y)
18 | / tuple(
19 | | list(
20 | | [x, 3, "hell"\
21 | | "o"]
22 | | )
23 | | )
| |_^ C414
16 | sorted(sorted(x, reverse=True), reverse=True)
17 | sorted(reversed(x))
18 | sorted(list(x), key=lambda y: y)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C414
19 | tuple(
20 | list(
|
= help: Remove the inner `list` call
Suggested fix
16 16 | sorted(reversed(x))
17 17 | sorted(list(x), key=lambda y: y)
18 18 | tuple(
19 |- list(
20 |- [x, 3, "hell"\
19 |+ [x, 3, "hell"\
21 20 | "o"]
22 21 | )
23 |-)
15 15 | sorted(sorted(x, key=foo, reverse=False), reverse=False, key=foo)
16 16 | sorted(sorted(x, reverse=True), reverse=True)
17 17 | sorted(reversed(x))
18 |-sorted(list(x), key=lambda y: y)
18 |+sorted(x, key=lambda y: y)
19 19 | tuple(
20 20 | list(
21 21 | [x, 3, "hell"\
C414.py:19:1: C414 [*] Unnecessary `list` call within `tuple()`
|
17 | sorted(reversed(x))
18 | sorted(list(x), key=lambda y: y)
19 | / tuple(
20 | | list(
21 | | [x, 3, "hell"\
22 | | "o"]
23 | | )
24 | | )
| |_^ C414
25 |
26 | # Nested sorts with differing keyword arguments. Not flagged.
|
= help: Remove the inner `list` call
Suggested fix
17 17 | sorted(reversed(x))
18 18 | sorted(list(x), key=lambda y: y)
19 19 | tuple(
20 |- list(
21 |- [x, 3, "hell"\
20 |+ [x, 3, "hell"\
22 21 | "o"]
23 22 | )
24 |-)
25 23 |
26 24 | # Nested sorts with differing keyword arguments. Not flagged.
27 25 | sorted(sorted(x, key=lambda y: y))

View File

@@ -1,92 +1,12 @@
#[cfg(target_family = "unix")]
#![cfg(target_family = "unix")]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_family = "unix")]
use std::path::Path;
#[cfg(target_family = "unix")]
use anyhow::Result;
use ruff_text_size::{TextLen, TextSize};
/// A shebang directive (e.g., `#!/usr/bin/env python3`).
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ShebangDirective<'a> {
/// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the
/// line.
pub(crate) offset: TextSize,
/// The contents of the directive (e.g., `"/usr/bin/env python3"`).
pub(crate) contents: &'a str,
}
impl<'a> ShebangDirective<'a> {
/// Parse a shebang directive from a line, or return `None` if the line does not contain a
/// shebang directive.
pub(crate) fn try_extract(line: &'a str) -> Option<Self> {
// Trim whitespace.
let directive = Self::lex_whitespace(line);
// Trim the `#!` prefix.
let directive = Self::lex_char(directive, '#')?;
let directive = Self::lex_char(directive, '!')?;
Some(Self {
offset: line.text_len() - directive.text_len(),
contents: directive,
})
}
/// Lex optional leading whitespace.
#[inline]
fn lex_whitespace(line: &str) -> &str {
line.trim_start()
}
/// Lex a specific character, or return `None` if the character is not the first character in
/// the line.
#[inline]
fn lex_char(line: &str, c: char) -> Option<&str> {
let mut chars = line.chars();
if chars.next() == Some(c) {
Some(chars.as_str())
} else {
None
}
}
}
#[cfg(target_family = "unix")]
pub(super) fn is_executable(filepath: &Path) -> Result<bool> {
let metadata = filepath.metadata()?;
let permissions = metadata.permissions();
Ok(permissions.mode() & 0o111 != 0)
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use crate::rules::flake8_executable::helpers::ShebangDirective;
#[test]
fn shebang_non_match() {
let source = "not a match";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_end_of_line() {
let source = "print('test') #!/usr/bin/python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_match() {
let source = "#!/usr/bin/env python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
#[test]
fn shebang_leading_space() {
let source = " #!/usr/bin/env python";
assert_debug_snapshot!(ShebangDirective::try_extract(source));
}
}

View File

@@ -2,6 +2,8 @@
use std::path::Path;
use wsl;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
@@ -42,6 +44,11 @@ impl Violation for ShebangMissingExecutableFile {
/// EXE002
#[cfg(target_family = "unix")]
pub(crate) fn shebang_missing(filepath: &Path) -> Option<Diagnostic> {
// WSL supports Windows file systems, which do not have executable bits.
// Instead, everything is executable. Therefore, we skip this rule on WSL.
if wsl::is_wsl() {
return None;
}
if let Ok(true) = is_executable(filepath) {
let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default());
return Some(diagnostic);

View File

@@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::rules::flake8_executable::helpers::ShebangDirective;
use crate::comments::shebang::ShebangDirective;
/// ## What it does
/// Checks for a shebang directive that is not at the beginning of the file.

View File

@@ -2,15 +2,17 @@
use std::path::Path;
use wsl;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::comments::shebang::ShebangDirective;
use crate::registry::AsRule;
#[cfg(target_family = "unix")]
use crate::rules::flake8_executable::helpers::is_executable;
use crate::rules::flake8_executable::helpers::ShebangDirective;
/// ## What it does
/// Checks for a shebang directive in a file that is not executable.
@@ -48,6 +50,11 @@ pub(crate) fn shebang_not_executable(
range: TextRange,
shebang: &ShebangDirective,
) -> Option<Diagnostic> {
// WSL supports Windows file systems, which do not have executable bits.
// Instead, everything is executable. Therefore, we skip this rule on WSL.
if wsl::is_wsl() {
return None;
}
let ShebangDirective { offset, contents } = shebang;
if let Ok(false) = is_executable(filepath) {

View File

@@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::rules::flake8_executable::helpers::ShebangDirective;
use crate::comments::shebang::ShebangDirective;
/// ## What it does
/// Checks for a shebang directive in `.py` files that does not contain `python`.

View File

@@ -1,10 +1,11 @@
use ruff_text_size::{TextRange, TextSize};
use std::ops::Sub;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::rules::flake8_executable::helpers::ShebangDirective;
use crate::comments::shebang::ShebangDirective;
/// ## What it does
/// Checks for whitespace before a shebang directive.

View File

@@ -1,7 +1,18 @@
//! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/).
use rustpython_parser::ast::{self, Expr};
pub(crate) mod rules;
pub mod settings;
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id)
} else {
false
}
}
#[cfg(test)]
mod tests {
use std::path::Path;

View File

@@ -5,6 +5,40 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for f-strings in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with an f-string as its argument can cause unexpected
/// behavior. Since the f-string is resolved before the function call, the
/// translation catalog will look up the formatted string, rather than the
/// f-string template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _(f"Hello, {name}!") # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct FStringInGetTextFuncCall;

View File

@@ -5,6 +5,40 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `str.format` calls in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with a formatted string as its argument can cause
/// unexpected behavior. Since the formatted string is resolved before the
/// function call, the translation catalog will look up the formatted string,
/// rather than the `str.format`-style template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!" % name) # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct FormatInGetTextFuncCall;

View File

@@ -1,10 +0,0 @@
use rustpython_parser::ast::{self, Expr};
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id)
} else {
false
}
}

View File

@@ -1,9 +1,7 @@
pub(crate) use f_string_in_gettext_func_call::*;
pub(crate) use format_in_gettext_func_call::*;
pub(crate) use is_gettext_func_call::*;
pub(crate) use printf_in_gettext_func_call::*;
mod f_string_in_gettext_func_call;
mod format_in_gettext_func_call;
mod is_gettext_func_call;
mod printf_in_gettext_func_call;

View File

@@ -4,6 +4,40 @@ use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for printf-style formatted strings in `gettext` function calls.
///
/// ## Why is this bad?
/// In the `gettext` API, the `gettext` function (often aliased to `_`) returns
/// a translation of its input argument by looking it up in a translation
/// catalog.
///
/// Calling `gettext` with a formatted string as its argument can cause
/// unexpected behavior. Since the formatted string is resolved before the
/// function call, the translation catalog will look up the formatted string,
/// rather than the printf-style template.
///
/// Instead, format the value returned by the function call, rather than
/// its argument.
///
/// ## Example
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, {}!".format(name)) # Looks for "Hello, Maria!".
/// ```
///
/// Use instead:
/// ```python
/// from gettext import gettext as _
///
/// name = "Maria"
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
/// ```
///
/// ## References
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
#[violation]
pub struct PrintfInGetTextFuncCall;

View File

@@ -57,7 +57,7 @@ impl Violation for SingleLineImplicitStringConcatenation {
///
/// By default, this rule will only trigger if the string literal is
/// concatenated via a backslash. To disallow implicit string concatenation
/// altogether, set the `flake8-implicit-str-concat.allow-multiline` option
/// altogether, set the [`flake8-implicit-str-concat.allow-multiline`] option
/// to `false`.
///
/// ## Example

View File

@@ -61,10 +61,7 @@ pub(crate) fn duplicate_class_field_definition<'a, 'b>(
// Extract the property name from the assignment statement.
let target = match stmt {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
if targets.len() != 1 {
continue;
}
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
if let [Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() {
id
} else {
continue;

View File

@@ -19,6 +19,8 @@ mod tests {
#[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))]
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))]
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))]
#[test_case(Rule::BadExitAnnotation, Path::new("PYI036.py"))]
#[test_case(Rule::BadExitAnnotation, Path::new("PYI036.pyi"))]
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))]
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
@@ -47,6 +49,8 @@ mod tests {
#[test_case(Rule::PassStatementStubBody, Path::new("PYI009.pyi"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))]
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.py"))]
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.pyi"))]
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))]
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))]
#[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))]

View File

@@ -52,10 +52,9 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// If the parent node is not a `BinOp` we will not perform a fix
if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker
.generator()
.expr(if expr == left.as_ref() { right } else { left }),
checker.locator.slice(child.range()).to_string(),
parent.unwrap().range(),
)));
}

View File

@@ -0,0 +1,346 @@
use std::fmt::{Display, Formatter};
use rustpython_parser::ast::{
ArgWithDefault, Arguments, Expr, ExprBinOp, ExprSubscript, ExprTuple, Identifier, Operator,
Ranged,
};
use smallvec::SmallVec;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for incorrect function signatures on `__exit__` and `__aexit__`
/// methods.
///
/// ## Why is this bad?
/// Improperly-annotated `__exit__` and `__aexit__` methods can cause
/// unexpected behavior when interacting with type checkers.
///
/// ## Example
/// ```python
/// class Foo:
/// def __exit__(self, typ, exc, tb, extra_arg) -> None:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def __exit__(
/// self,
/// typ: type[BaseException] | None,
/// exc: BaseException | None,
/// tb: TracebackType | None,
/// extra_arg: int = 0,
/// ) -> None:
/// ...
/// ```
#[violation]
pub struct BadExitAnnotation {
func_kind: FuncKind,
error_kind: ErrorKind,
}
impl Violation for BadExitAnnotation {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let method_name = self.func_kind.to_string();
match self.error_kind {
ErrorKind::StarArgsNotAnnotated => format!("Star-args in `{method_name}` should be annotated with `object`"),
ErrorKind::MissingArgs => format!("If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)"),
ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!("All arguments after the first four in `{method_name}` must have a default value"),
ErrorKind::AllKwargsMustHaveDefault => format!("All keyword-only arguments in `{method_name}` must have a default value"),
ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"),
ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"),
ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"),
}
}
fn autofix_title(&self) -> Option<String> {
if matches!(self.error_kind, ErrorKind::StarArgsNotAnnotated) {
Some("Annotate star-args with `object`".to_string())
} else {
None
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum FuncKind {
Sync,
Async,
}
impl Display for FuncKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FuncKind::Sync => write!(f, "__exit__"),
FuncKind::Async => write!(f, "__aexit__"),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ErrorKind {
StarArgsNotAnnotated,
MissingArgs,
FirstArgBadAnnotation,
SecondArgBadAnnotation,
ThirdArgBadAnnotation,
ArgsAfterFirstFourMustHaveDefault,
AllKwargsMustHaveDefault,
}
/// PYI036
pub(crate) fn bad_exit_annotation(
checker: &mut Checker,
is_async: bool,
name: &Identifier,
args: &Arguments,
) {
let func_kind = match name.as_str() {
"__exit__" if !is_async => FuncKind::Sync,
"__aexit__" if is_async => FuncKind::Async,
_ => return,
};
let positional_args = args
.args
.iter()
.chain(args.posonlyargs.iter())
.collect::<SmallVec<[&ArgWithDefault; 4]>>();
// If there are less than three positional arguments, at least one of them must be a star-arg,
// and it must be annotated with `object`.
if positional_args.len() < 4 {
check_short_args_list(checker, args, func_kind);
}
// Every positional argument (beyond the first four) must have a default.
for arg_with_default in positional_args
.iter()
.skip(4)
.filter(|arg_with_default| arg_with_default.default.is_none())
{
checker.diagnostics.push(Diagnostic::new(
BadExitAnnotation {
func_kind,
error_kind: ErrorKind::ArgsAfterFirstFourMustHaveDefault,
},
arg_with_default.range(),
));
}
// ...as should all keyword-only arguments.
for arg_with_default in args.kwonlyargs.iter().filter(|arg| arg.default.is_none()) {
checker.diagnostics.push(Diagnostic::new(
BadExitAnnotation {
func_kind,
error_kind: ErrorKind::AllKwargsMustHaveDefault,
},
arg_with_default.range(),
));
}
check_positional_args(checker, &positional_args, func_kind);
}
/// Determine whether a "short" argument list (i.e., an argument list with less than four elements)
/// contains a star-args argument annotated with `object`. If not, report an error.
fn check_short_args_list(checker: &mut Checker, args: &Arguments, func_kind: FuncKind) {
if let Some(varargs) = &args.vararg {
if let Some(annotation) = varargs
.annotation
.as_ref()
.filter(|ann| !is_object_or_unused(ann, checker.semantic()))
{
let mut diagnostic = Diagnostic::new(
BadExitAnnotation {
func_kind,
error_kind: ErrorKind::StarArgsNotAnnotated,
},
annotation.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_builtin("object") {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
"object".to_string(),
annotation.range(),
)));
}
}
checker.diagnostics.push(diagnostic);
}
} else {
checker.diagnostics.push(Diagnostic::new(
BadExitAnnotation {
func_kind,
error_kind: ErrorKind::MissingArgs,
},
args.range(),
));
}
}
/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method are
/// annotated correctly.
fn check_positional_args(
checker: &mut Checker,
positional_args: &[&ArgWithDefault],
kind: FuncKind,
) {
// For each argument, define the predicate against which to check the annotation.
type AnnotationValidator = fn(&Expr, &SemanticModel) -> bool;
let validations: [(ErrorKind, AnnotationValidator); 3] = [
(ErrorKind::FirstArgBadAnnotation, is_base_exception_type),
(ErrorKind::SecondArgBadAnnotation, is_base_exception),
(ErrorKind::ThirdArgBadAnnotation, is_traceback_type),
];
for (arg, (error_info, predicate)) in positional_args.iter().skip(1).take(3).zip(validations) {
let Some(annotation) = arg.def.annotation.as_ref() else {
continue;
};
if is_object_or_unused(annotation, checker.semantic()) {
continue;
}
// If there's an annotation that's not `object` or `Unused`, check that the annotated type
// matches the predicate.
if non_none_annotation_element(annotation, checker.semantic())
.map_or(false, |elem| predicate(elem, checker.semantic()))
{
continue;
}
checker.diagnostics.push(Diagnostic::new(
BadExitAnnotation {
func_kind: kind,
error_kind: error_info,
},
annotation.range(),
));
}
}
/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation.
fn non_none_annotation_element<'a>(
annotation: &'a Expr,
model: &SemanticModel,
) -> Option<&'a Expr> {
// E.g., `typing.Union` or `typing.Optional`
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation {
if model.match_typing_expr(value, "Optional") {
return if is_const_none(slice) {
None
} else {
Some(slice)
};
}
if !model.match_typing_expr(value, "Union") {
return None;
}
let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else {
return None;
};
let [left, right] = elts.as_slice() else {
return None;
};
return match (is_const_none(left), is_const_none(right)) {
(false, true) => Some(left),
(true, false) => Some(right),
(true, true) => None,
(false, false) => None,
};
}
// PEP 604-style union (e.g., `int | None`)
if let Expr::BinOp(ExprBinOp {
op: Operator::BitOr,
left,
right,
..
}) = annotation
{
if !is_const_none(left) {
return Some(left);
}
if !is_const_none(right) {
return Some(right);
}
return None;
}
None
}
/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type.
fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.map_or(false, |call_path| {
matches!(
call_path.as_slice(),
["" | "builtins", "object"] | ["_typeshed", "Unused"]
)
})
}
/// Return `true` if the [`Expr`] is `BaseException`.
fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["" | "builtins", "BaseException"])
})
}
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["types", "TracebackType"])
})
}
/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`.
fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool {
let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else {
return false;
};
if model.match_typing_expr(value, "Type")
|| model
.resolve_call_path(value)
.as_ref()
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["" | "builtins", "type"])
})
{
is_base_exception(slice, model)
} else {
false
}
}

View File

@@ -5,6 +5,7 @@ pub(crate) use complex_if_statement_in_stub::*;
pub(crate) use docstring_in_stubs::*;
pub(crate) use duplicate_union_member::*;
pub(crate) use ellipsis_in_non_empty_class_body::*;
pub(crate) use exit_annotations::*;
pub(crate) use future_annotations_in_stub::*;
pub(crate) use iter_method_return_iterable::*;
pub(crate) use no_return_argument_annotation::*;
@@ -15,6 +16,7 @@ pub(crate) use pass_in_class_body::*;
pub(crate) use pass_statement_stub_body::*;
pub(crate) use prefix_type_params::*;
pub(crate) use quoted_annotation_in_stub::*;
pub(crate) use redundant_numeric_union::*;
pub(crate) use simple_defaults::*;
pub(crate) use str_or_repr_defined_in_stub::*;
pub(crate) use string_or_bytes_too_long::*;
@@ -33,6 +35,7 @@ mod complex_if_statement_in_stub;
mod docstring_in_stubs;
mod duplicate_union_member;
mod ellipsis_in_non_empty_class_body;
mod exit_annotations;
mod future_annotations_in_stub;
mod iter_method_return_iterable;
mod no_return_argument_annotation;
@@ -43,6 +46,7 @@ mod pass_in_class_body;
mod pass_statement_stub_body;
mod prefix_type_params;
mod quoted_annotation_in_stub;
mod redundant_numeric_union;
mod simple_defaults;
mod str_or_repr_defined_in_stub;
mod string_or_bytes_too_long;

View File

@@ -60,10 +60,10 @@ impl Violation for UnprefixedTypeParam {
/// PYI001
pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
if targets.len() != 1 {
let [target] = targets else {
return;
}
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
};
if let Expr::Name(ast::ExprName { id, .. }) = target {
if id.starts_with('_') {
return;
}

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