Compare commits

..

128 Commits

Author SHA1 Message Date
Charlie Marsh
3992c47c00 Bump version to 0.0.276 (#5488) 2023-07-03 18:02:49 +00:00
Charlie Marsh
8de5a3d29d Allow Final assignments in stubs (#5490)
## Summary

This fixes one incompatibility with `flake8-pyi`, and gives us a clean
pass on `typeshed`.
2023-07-03 17:57:49 +00:00
Charlie Marsh
ed1dd09d02 Refine some perflint rules (#5484)
## Summary

Removing some false positives based on running over `zulip`.

`PERF401` now also detects cases like:

```py
original = list(range(10000))
filtered = []
for i in original:
    filtered.append(i * i)
```

Previously, these were caught by the list-copy rule, but these too need
comprehensions.
2023-07-03 13:53:17 -04:00
Charlie Marsh
ca497fabbd Remove some diagnostics.extend calls (#5483)
## Summary

It's more efficient (and more idiomatic for us) to pass in the `Checker`
directly.
2023-07-03 16:47:23 +00:00
Charlie Marsh
00fbbe4223 Remove some additional manual iterator matches (#5482)
## Summary

I've done a few of these PRs, I thought I'd caught them all, but missed
this pattern.
2023-07-03 16:29:59 +00:00
Charlie Marsh
dadad0e9ed Remove some allocations in argument detection (#5481)
## Summary

Drive-by PR to remove some allocations around argument name matching.
2023-07-03 12:21:26 -04:00
Charlie Marsh
d2450c25ab Audit remove_argument usages to use end-of-function (#5480)
## Summary

This PR applies the fix in #5478 to a variety of other call-sites, and
fixes some other range hygienic stuff in the rules that were modified.
2023-07-03 12:21:01 -04:00
Harutaka Kawamura
1e4b88969c Fix unnecessary-encode-utf8 to fix encode on parenthesized strings correctly (#5478)
## Summary

Fixes #5477

## Test Plan

New test cases.
2023-07-03 10:11:09 -04:00
Louis Dispa
dc072537e5 Fix python_formatter generate.py with rust path (#5475)
## Summary

This PR fix an issue with the `generate.py` file of the python
formatter.
Since https://github.com/astral-sh/ruff/pull/5369 the [node.rs
file](f51dc20497/crates/ruff_python_ast/src/node.rs)
used to generate the types now has `ast::` in the enum.

```rust
pub enum AnyNode {
   ModModule(ModModule),
   ModInteractive(ModInteractive),
   ModExpression(ModExpression),
   ModFunctionType(ModFunctionType),
   ...
```

And now:

```rust
pub enum AnyNode {
   ModModule(ast::ModModule),
   ModInteractive(ast::ModInteractive),
   ModExpression(ast::ModExpression),
   ModFunctionType(ast::ModFunctionType),
   ...
```

The python script was not parsing rust paths. This PR adds the
possibility to have it.

## Test Plan

This was tested locally.

### Script output

Before

```
['ast::ModModule),', 'ast::ModInteractive),', 'ast::ModExpression),', 'ast::ModFunctionType),', 'ast::StmtFunctionDef),', 'ast::StmtAsyncFunctionDef),', 'ast::StmtClassDef),', 'ast::StmtReturn),', 'ast::StmtDelete),', 'ast::StmtAssign),', 'ast::StmtAugAssign),', 'ast::StmtAnnAssign),', 'ast::StmtFor),', 'ast::StmtAsyncFor),', 'ast::StmtWhile),', 'ast::StmtIf),', 'ast::StmtWith),', 'ast::StmtAsyncWith),', 'ast::StmtMatch),', 'ast::StmtRaise),', 'ast::StmtTry),', 'ast::StmtTryStar),', 'ast::StmtAssert),', 'ast::StmtImport),', 'ast::StmtImportFrom),', 'ast::StmtGlobal),', 'ast::StmtNonlocal),', 'ast::StmtExpr),', 'ast::StmtPass),', 'ast::StmtBreak),', 'ast::StmtContinue),', 'ast::ExprBoolOp),', 'ast::ExprNamedExpr),', 'ast::ExprBinOp),', 'ast::ExprUnaryOp),', 'ast::ExprLambda),', 'ast::ExprIfExp),', 'ast::ExprDict),', 'ast::ExprSet),', 'ast::ExprListComp),', 'ast::ExprSetComp),', 'ast::ExprDictComp),', 'ast::ExprGeneratorExp),', 'ast::ExprAwait),', 'ast::ExprYield),', 'ast::ExprYieldFrom),', 'ast::ExprCompare),', 'ast::ExprCall),', 'ast::ExprFormattedValue),', 'ast::ExprJoinedStr),', 'ast::ExprConstant),', 'ast::ExprAttribute),', 'ast::ExprSubscript),', 'ast::ExprStarred),', 'ast::ExprName),', 'ast::ExprList),', 'ast::ExprTuple),', 'ast::ExprSlice),', 'ast::ExceptHandlerExceptHandler),', 'ast::PatternMatchValue),', 'ast::PatternMatchSingleton),', 'ast::PatternMatchSequence),', 'ast::PatternMatchMapping),', 'ast::PatternMatchClass),', 'ast::PatternMatchStar),', 'ast::PatternMatchAs),', 'ast::PatternMatchOr),', 'ast::TypeIgnoreTypeIgnore),', 'Comprehension),', 'Arguments),', 'Arg),', 'ArgWithDefault),', 'Keyword),', 'Alias),', 'WithItem),', 'MatchCase),', 'Decorator),']

error: unexpected closing delimiter: `)`
 --> <stdin>:3:55
  |
2 |             use ruff_formatter::{write, Buffer, FormatResult};
  |                                 - this opening brace...     - ...matches this closing brace
3 |             use rustpython_parser::ast::ast::ModModule),;
  |                                                       ^ unexpected closing delimiter

Traceback (most recent call last):
  File "/Users/ldispa/Documents/perso/ruff/crates/ruff_python_formatter/generate.py", line 100, in <module>
    node_path.write_text(rustfmt(code))
                         ^^^^^^^^^^^^^
  File "/Users/ldispa/Documents/perso/ruff/crates/ruff_python_formatter/generate.py", line 12, in rustfmt
    return check_output(["rustfmt", "--emit=stdout"], input=code, text=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/subprocess.py", line 466, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/subprocess.py", line 571, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['rustfmt', '--emit=stdout']' returned non-zero exit status 1.
```

After:
```
['ModModule', 'ModInteractive', 'ModExpression', 'ModFunctionType', 'StmtFunctionDef', 'StmtAsyncFunctionDef', 'StmtClassDef', 'StmtReturn', 'StmtDelete', 'StmtAssign', 'StmtAugAssign', 'StmtAnnAssign', 'StmtFor', 'StmtAsyncFor', 'StmtWhile', 'StmtIf', 'StmtWith', 'StmtAsyncWith', 'StmtMatch', 'StmtRaise', 'StmtTry', 'StmtTryStar', 'StmtAssert', 'StmtImport', 'StmtImportFrom', 'StmtGlobal', 'StmtNonlocal', 'StmtExpr', 'StmtPass', 'StmtBreak', 'StmtContinue', 'ExprBoolOp', 'ExprNamedExpr', 'ExprBinOp', 'ExprUnaryOp', 'ExprLambda', 'ExprIfExp', 'ExprDict', 'ExprSet', 'ExprListComp', 'ExprSetComp', 'ExprDictComp', 'ExprGeneratorExp', 'ExprAwait', 'ExprYield', 'ExprYieldFrom', 'ExprCompare', 'ExprCall', 'ExprFormattedValue', 'ExprJoinedStr', 'ExprConstant', 'ExprAttribute', 'ExprSubscript', 'ExprStarred', 'ExprName', 'ExprList', 'ExprTuple', 'ExprSlice', 'ExceptHandlerExceptHandler', 'PatternMatchValue', 'PatternMatchSingleton', 'PatternMatchSequence', 'PatternMatchMapping', 'PatternMatchClass', 'PatternMatchStar', 'PatternMatchAs', 'PatternMatchOr', 'TypeIgnoreTypeIgnore', 'Comprehension', 'Arguments', 'Arg', 'ArgWithDefault', 'Keyword', 'Alias', 'WithItem', 'MatchCase', 'Decorator']
```
2023-07-03 16:07:57 +02:00
konsti
7ac9e0252e Document Checking formatter stability and panics (#5415)
This adds the documentation, but ideally we should add the CI first
2023-07-03 11:22:19 +02:00
konsti
ca6ff72404 Change generator formatting dummy to include NOT_YET_IMPLEMENTED (#5464)
## Summary

Change generator formatting dummy to include `NOT_YET_IMPLEMENTED`. This
makes it easier to correctly identify them as dummies

## Test Plan

This is a dummy change
2023-07-03 09:11:14 +02:00
Charlie Marsh
94ac2c4e1b Reorganize some flake8-pyi rules (#5472) 2023-07-03 04:39:22 +00:00
qdegraaf
93b2bd7184 [perflint] Add PERF401 and PERF402 rules (#5298)
## Summary

Adds `PERF401` and `PERF402` mirroring `W8401` and `W8402` from
https://github.com/tonybaloney/perflint

Implementation is not super smart but should be at parity with upstream
implementation judging by:
c07391c176/perflint/comprehension_checker.py (L42-L73)

It essentially checks:

- If the body of a for-loop is just one statement
- If that statement is an `if` and the if-statement contains a call to
`append()` we flag `PERF401` and suggest a list comprehension
- If that statement is a plain call to `append()` or `insert()` we flag
`PERF402` and suggest `list()` or `list.copy()`

I've set the violation to only flag the first append call in a long
`if-else` statement for `PERF401`. Happy to change this to some other
location or make it multiple violations if that makes more sense.

## Test Plan

Fixtures were added with the relevant scenarios for both rules

## Issue Links

Refers: https://github.com/astral-sh/ruff/issues/4789
2023-07-03 04:03:09 +00:00
Justin Prieto
0bff4ed4d3 [flake8-pyi] Implement PYI002, PYI003, PYI004, PYI005 (#5457)
## Summary

Implements flake8-pyi checks 002, 003, 004, 005. The logic is a bit
complex, as you can see in the [original
code](57921813c1/pyi.py (L1403C18-L1403C18)).

ref: #848 

## Test Plan

Updated snapshot tests. Ran flake8 to double check lints, and ran ruff
with all PYI lints enabled to check for incorrect overlapping lint
errors.
2023-07-02 23:52:16 -04:00
Anders Kaseorg
df13e69c3c Format let-else with rustfmt nightly (#5461)
Support for `let…else` formatting was just merged to nightly
(rust-lang/rust#113225). Rerun `cargo fmt` with Rust nightly 2023-07-02
to pick this up. Followup to #939.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-07-03 02:13:35 +00:00
Charlie Marsh
c8b9a46e2b [pyupgrade] Restore the keep-runtime-typing setting (#5470)
## Summary

This PR reverts #4427. See the included documentation for a detailed
explanation.

Closes #5434.
2023-07-03 02:11:31 +00:00
Charlie Marsh
6cc04d64e4 [flake8-django] Skip duplicate violations in DJ012 (#5469)
## Summary

This PR reduces the noise from `DJ012` by emitting a single violation
when you have multiple consecutive violations of the same "type".

For example, given:

```py
class MultipleConsecutiveFields(models.Model):
    """Model that contains multiple out-of-order field definitions in a row."""


    class Meta:
        verbose_name = "test"

    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)
```

It's convenient to only error on `first_name`, and not `last_name`,
since we're really flagging that the _section_ is out-of-order.

Closes #5465.
2023-07-02 21:09:49 -04:00
Charlie Marsh
d0b2fffb87 [numpy] Add numpy-deprecated-function (NPY003) (#5468)
## Summary

Closes #5456.
2023-07-02 20:50:14 -04:00
Charlie Marsh
b32d1e8d78 Detect consecutive, non-newline-delimited NumPy sections (#5467)
## Summary

Given a docstring like:

```py
def f(a: int, b: int) -> int:
    """Showcase function.
    Parameters
    ----------
    a : int
        _description_
    b : int
        _description_
    Returns
    -------
    int
        _description
    """
```

We were failing to identify `Returns` as a section, because the previous
line was neither empty nor ended with punctuation. This was causing a
false negative, where by we weren't flagging a missing line before
`Returns`. So, the very reason for the rule (no blank line) was causing
us to fail to catch it.

Note that, we did have a test case for this, which was working properly:

```py
def f() -> int:
    """Showcase function.
    Parameters
    ----------
    Returns
    -------
    """
```

...because the line before `Returns` "ends in a punctuation mark" (`-`).

Closes #5442.
2023-07-02 20:29:45 -04:00
Charlie Marsh
af7051b976 Include BaseException in B017 rule (#5466)
Closes #5462.
2023-07-02 20:18:33 -04:00
Micha Reiser
f0ec9ecd67 Show BestFitting mode if it isn't FirstLine (#5452) 2023-06-30 09:49:00 +00:00
Micha Reiser
f9129e435a Normalize '\r' in string literals to '\n'
<!--
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 normalizes line endings inside of strings to `\n` as required by the printer.

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

## Test Plan

I added a new test using `\r\n` and ran the ecosystem check. There are no remaining end of line panics. 


https://gist.github.com/MichaReiser/8f36b1391ca7b48475b3a4f592d74ff4

<!-- How was it tested? -->
2023-06-30 10:13:23 +02:00
Micha Reiser
dc65007fe9 Use rayon to parallelize the stability check
<!--
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 rayon to parallelize the stability check by scheduling each project as its own task.

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

## Test Plan

I ran the ecosystem check. It now makes use of all cores (except at the end, there are some large projects). 

## Performance

The check now completes in minutes where it took about 30 minutes before.

<!-- How was it tested? -->
2023-06-30 10:05:25 +02:00
Micha Reiser
9c2a75284b Preserve parentheses around left side of binary expression
<!--
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 fixes an issue where the binary expression formatting removed parentheses around the left hand side of an expression.

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

## Test Plan

I added a new regression test and re-ran the ecosystem check. It brings down the `check-formatter-stability` output from a 3.4MB file down to 900KB. 

<!-- How was it tested? -->
2023-06-30 09:52:14 +02:00
Micha Reiser
ae25638b0b Update Black tests (#5438) 2023-06-30 06:32:50 +00:00
Micha Reiser
f7969cf23c ecosystem: Run git command with no human interaction flag (#5435) 2023-06-29 09:19:11 +02:00
Micha Reiser
955e9ef821 Fix invalid syntax for binary expression in unary op (#5370) 2023-06-29 08:09:26 +02:00
Micha Reiser
38189ed913 Fix invalid printer IR error (#5422) 2023-06-29 08:09:13 +02:00
David Szotten
ca5e10b5ea format StmtTryStar (#5418) 2023-06-29 08:07:33 +02:00
Charlie Marsh
a973019358 Rewrite a variety of .contains() calls as matches! statements (#5432)
## Summary

These have the potential to be much more efficient, as we've seen in the
past.
2023-06-28 22:42:27 -04:00
Charlie Marsh
aa887d5a1d Use "manual" fixability for E731 in shadowed context (#5430)
## Summary

This PR makes E731 a "manual" fix in one other context: when the lambda
is shadowing another variable in the scope. Function declarations (with
shadowing) cause issues for type checkers, and so rewriting an
annotation, e.g., in branches of an `if` statement can lead to failures.

Closes https://github.com/astral-sh/ruff/issues/5421.
2023-06-28 22:00:06 -04:00
Charlie Marsh
72f7f11bac Use matches! for reserved attribute lookup (#5431) 2023-06-29 01:52:11 +00:00
Tom Kuson
5aa2a90e17 Add documentation to flake8-logging-format rules (#5417)
## Summary

Completes the documentation for the `flake8-logging-format` rules.
Related to #2646.

I included both the `flake8-logging-format` recommendation to use the
`extra` keyword and the Pylint recommendation to pass format values as
parameters so that formatting is done lazily, as #970 suggests the
Pylint logging rules are covered by this ruleset. Using lazy formatting
via parameters is probably more common than avoiding formatting entirely
in favour of the `extra` argument, regardless.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-29 01:30:11 +00:00
Charlie Marsh
0e89c94947 Run shadowed-variable analyses in deferred handlers (#5181)
## Summary

This PR extracts a bunch of complex logic from `add_binding`, instead
running the the shadowing rules in the deferred handler, thereby
decoupling the binding phase (during which we build up the semantic
model) from the analysis phase, and generally making `add_binding` much
more focused.

This was made possible by improving the semantic model to better handle
deletions -- previously, we'd "lose track" of bindings if they were
deleted, which made this kind of refactor impossible.

## Test Plan

We have good automated coverage for this, but I want to benchmark it
separately.
2023-06-29 00:08:18 +00:00
Eric H
139a9f757b Update default configuration.md to mention C901 rule (#5397) 2023-06-28 21:22:16 +00:00
Charlie Marsh
c5e20505f8 Remove an unsafe access in the resolver (#5428) 2023-06-28 19:08:10 +00:00
Charlie Marsh
69c4b7fa11 Add dedicated struct for implicit imports (#5427)
## Summary

This was some feedback on a prior PR that I decided to act on
separately.
2023-06-28 18:55:43 +00:00
Charlie Marsh
0e12eb3071 Add a snapshot test for native module resolution (#5423) 2023-06-28 18:16:39 +00:00
Charlie Marsh
864f50a3a4 Remove all unwrap calls from the resolver (#5426) 2023-06-28 18:06:17 +00:00
Charlie Marsh
4d90a5a9bc Move resolver tests out to top-level (#5424)
## Summary

These are really tests for the entire crate.
2023-06-28 13:25:37 -04:00
Charlie Marsh
1d2d015bc5 Make standard input detection robust to invalid arguments (#5393)
## Summary

This PR fixes a silent failure that manifested itself in
https://github.com/astral-sh/ruff-vscode/issues/238. In short, if the
user provided invalid arguments to Ruff in the VS Code extension (like
`"ruff.args": ["a"]`), then we generated something like the following
command:

```console
/path/to/ruff --force-exclude --no-cache --no-fix --format json - --fix a --stdin-filename /path/to/file.py
```

Since this contains both `-` and `a` as the "input files", Ruff would
treat this as if we're linting the files names `-` and `a`, rather than
linting standard input.

This PR modifies out standard input detection to force standard input
when `--stdin-filename` is present, or at least one file is `-`. (We
then warn and ignore the others.)
2023-06-28 14:52:23 +00:00
Charlie Marsh
ea7bb199bc Fill-in missing implementation for is_native_module_file_name (#5410)
## Summary

This was just an oversight -- the last remaining `todo!()` that I never
filled in. We clearly don't have any test coverage for it yet, but this
mimics the Pyright implementation.
2023-06-28 14:50:54 +00:00
Charlie Marsh
979049b2a6 Make lib iteration platform-specific (#5406) 2023-06-28 13:52:20 +00:00
Charlie Marsh
6587fb844a Add snapshot tests for resolver (#5404)
## Summary

This PR adds some snapshot tests for the resolver based on executing
resolutions within a "mock" of the Airflow repo (that is: a folder that
contains a subset of the repo's files, but all empty, and with an
only-partially-complete virtual environment). It's intended to act as a
lightweight integration test, to enable us to test resolutions on a
"real" project without adding a dependency on Airflow itself.
2023-06-28 13:38:51 +00:00
Dhruv Manilawala
a68a86e18b fixup! Consider Jupyter index for code frames (--show-source) (#5402) (#5414) 2023-06-28 10:25:05 +00:00
Christian Clauss
b42d76494c types.rs: fnmatch url should point to current Python docs (#5413)
Like #5412
2023-06-28 15:54:13 +05:30
David Szotten
c7adb9117f format StmtAsyncWith (#5376)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-06-28 10:21:44 +00:00
David Szotten
1979103ec0 Format StmtTry (#5222)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-06-28 10:02:15 +00:00
Christian Clauss
9e2fd0c620 ruff rule SLOT uses URL to current Python docs (#5412)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
Currently the URL at the bottom of the `ruff rule SLOT00x` output points
to Python 3.7 docs.
Given that Python 3.7 is now end-of-life (as of yesterday), let's
instead point users to the current Python docs.

## Test Plan

<!-- How was it tested? -->
2023-06-28 09:48:52 +00:00
Charlie Marsh
366edc5a3f Fix string annotation in docs (#5411) 2023-06-28 03:29:56 +00:00
Dhruv Manilawala
2aecaf5060 Consider Jupyter index for code frames (--show-source) (#5402)
## Summary

Consider Jupyter index for code frames (`--show-source`).

This solves two problems as mentioned in the linked issue:

> Omit any contents from adjoining cells

If the Jupyter index is present, we'll use that to check if the
surrounding
lines belong to the same cell as the content line. If not, we'll skip
that line
until we either reach the one which does or we reach the content line.

> code frame line number

If the Jupyter index is present, we'll use that to get the actual start
line in
corresponding to the computed start index.

## Test Plan

`cargo run --bin ruff -- check --no-cache --isolated --select=ALL --show-source /path/to/notebook.ipynb`

fixes: #5395
2023-06-28 08:54:51 +05:30
Dhruv Manilawala
d19324df69 Add Jupyter integration to the docs (#5403)
## Summary

Add Jupyter integration to the docs, specifically the Configuration and
FAQ sections.

## Test Plan

`mkdocs serve` and check that the new sections are visible and
functional.

fixes: #5396
2023-06-28 00:27:24 +00:00
Marti Raudsepp
2c99b268c6 Exclude docstrings from PYI053 (#5405)
## Summary

The `Y053` rule of `flake8-pyi` ignores docstrings, it only triggers on
other string literals.

The separate `Y021/PYI021` rule exists to disallow docstrings.

## Test Plan

Added some `# OK` test cases to `PYI053.py(i)` files.
2023-06-28 00:19:20 +00:00
Charlie Marsh
56f73de0cb Misc. clean-up for import resolver (#5401)
## Summary

Renaming functions, adding documentation, refactoring the test
infrastructure a bit.
2023-06-27 19:27:12 +00:00
Tom Kuson
a0a93a636f Implement Pylint single-string-used-for-slots (C0205) as single-string-slots (PLC0205) (#5399)
## Summary

Implement Pylint rule `single-string-used-for-slots` (`C0205`) as
`single-string-slots` (`PLC0205`). This rule checks for single strings
being assigned to `__slots__`. For example

```python
class Foo:
    __slots__: str = "bar"

    def __init__(self, bar: str) -> None:
        self.bar = bar
```

should be

```python
class Foo:
    __slots__: tuple[str, ...] = ("bar",)

    def __init__(self, bar: str) -> None:
        self.bar = bar
```

Related to #970. Includes documentation.

## Test Plan

`cargo test`
2023-06-27 18:33:58 +00:00
Tom Kuson
035f8993f4 Complete documentation for pydocstyle rules (#5387)
## Summary

Completes the documentation for the `pydocstyle` ruleset. Related to
#2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-27 18:12:21 +00:00
Charlie Marsh
032b967b05 Enable --watch for Jupyter notebooks (#5394)
## Summary

The list of extensions that support watching is hard-coded
(unfortunately); this PR adds `.ipynb` to the list.
2023-06-27 12:53:47 -04:00
Dhruv Manilawala
962479d943 Replace same length equal line with dash line in D407 (#5383)
## Summary

Replace same length equal line with dash line in D407

Do we want to update the message and autofix title to reflect this
change?

## Test Plan

Added test cases for:
- Equal line length == dash line length
- Equal line length != dash line length

fixes: #5378
2023-06-27 16:50:20 +00:00
Evan Rittenhouse
ff0d0ab7a0 Add applicability to pydocstyle (#5390) 2023-06-27 12:40:19 -04:00
Evan Rittenhouse
0585e14d3b Add applicability to flake8_pytest_style (#5389) 2023-06-27 12:39:56 -04:00
Charlie Marsh
1ed227a1e0 Port Pyright's import resolver to Rust (#5381)
## Summary

This PR contains the first step towards enabling robust first-party,
third-party, and standard library import resolution in Ruff (including
support for `typeshed`, stub files, native modules, etc.) by porting
Pyright's import resolver to Rust.

The strategy taken here was to start with a more-or-less direct port of
the Pyright's TypeScript resolver. The code is intentionally similar,
and the test suite is effectively a superset of Pyright's test suite for
its own resolver. Due to the nature of the port, the code is very, very
non-idiomatic for Rust. The code is also entirely unused outside of the
test suite, and no effort has been made to integrate it with the rest of
the codebase.

Future work will include:

- Refactoring the code (now that it works) to match Rust and Ruff
idioms.
- Further testing, in practice, to ensure that the resolver can resolve
imports in a complex project, when provided with a virtual environment
path.
- Caching, to minimize filesystem lookups and redundant resolutions.
- Integration into Ruff itself (use Ruff's existing settings, find rules
that can make use of robust resolution, etc.)
2023-06-27 16:15:07 +00:00
Charlie Marsh
502e15585d Ignore unpacking in iteration-over-set (#5392)
Closes #5386.
2023-06-27 15:33:42 +00:00
konstin
520f4f33c3 Fix ruff_dev repeat by removing short argument (#5388)
ruff_dev repeat recently broke (i think with the cargo update?):

> thread 'main' panicked at 'Command repeat: Short option names must be
unique for each argument, but '-n' is in use by both 'no_cache' and
'repeat''

This fixes this by removing the short argument.
2023-06-27 13:29:20 +00:00
konstin
7f6cb9dfb5 Format call expressions (without call chaining) (#5341)
## Summary

This formats call expressions with magic trailing comma and parentheses
behaviour but without call chaining

## Test Plan

Lots of new test fixtures, including some that don't work yet
2023-06-27 09:29:40 +00:00
David Szotten
50a7769d69 magic trailing comma for ExprList (#5365) 2023-06-26 21:59:01 +02:00
Evan Rittenhouse
190bed124f [perflint] Implement try-except-in-loop (PERF203) (#5166)
## Summary

Implements PERF203 from #4789, which throws if a `try/except` block is
inside of a loop. Not sure if we want to extend the diagnostic to the
`except` as well, but I thought that that may get a little messy. We may
also want to just throw on the word `try` - open to suggestions though.

## Test Plan
`cargo test`
2023-06-26 17:34:37 +00:00
Charlie Marsh
d53b986fd4 Fix autofix capabilities in playground (#5375)
## Summary

These had just bitrotted over time -- we were no longer passing along
the row-and-column indices, etc.

## Test Plan

![Screen Shot 2023-06-26 at 12 03 41
PM](https://github.com/astral-sh/ruff/assets/1309177/6791330d-010b-45d3-91ef-531d4745193f)
2023-06-26 16:40:28 +00:00
Charlie Marsh
8a1bb7a5af Fix version number in playground (#5372)
## Summary

`v0.0.275` in the top-right was showing `v0.0.0` at all times.

## Test Plan

![Screen Shot 2023-06-26 at 11 31 16
AM](https://github.com/astral-sh/ruff/assets/1309177/e6cd0e19-6a5f-4b46-a060-54f492524737)
2023-06-26 15:56:12 +00:00
Dhruv Manilawala
2fc38d81e6 Experimental release for Jupyter notebook integration (#5363)
## Summary

Experimental release for Jupyter Notebook integration.

Currently, this requires a user to explicitly opt-in using the
[include](https://beta.ruff.rs/docs/settings/#include) configuration:

```toml
[tool.ruff]
include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"]
```

Or, a user can pass in the file directly:

```sh
ruff check path/to/notebook.ipynb
```

For known limitations, please refer #5188 

## Test Plan

Following command should work without the `--all-features` flag:

```sh
cargo dev round-trip /path/to/notebook.ipynb
```

Following command should work with the above config file along with
`select = ["ALL"]`:

```sh
cargo run --bin ruff -- check --no-cache --config=../test-repos/openai-cookbook/pyproject.toml --fix ../test-repos/openai-cookbook/
```

Passing the Jupyter notebook directly:

```sh
cargo run --bin ruff -- check --no-cache --isolated --select=ALL --fix ../test-repos/openai-cookbook/examples/Classification_using_embeddings.ipynb
```
2023-06-26 21:22:42 +05:30
Charlie Marsh
fa1b85b3da Remove prelude from ruff_python_ast (#5369)
## Summary

Per @MichaReiser, this is causing more confusion than it is helpful.
2023-06-26 11:43:49 -04:00
Tom Kuson
baa7264ca4 Add documentation for flake8-2020 (#5366)
## Summary

Completes the documentation for the `flake8-2020` ruleset. Related to
#2646 .

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-26 15:24:42 +00:00
Tom Kuson
fde3f09370 Add documentation missing docstring rules (D1XX) (#5330)
## Summary

Add documentation to the `D1XX` rules that flag missing docstrings. 

The examples are quite long and docstrings practices vary a lot between
projects, so I thought it would be best that the documentation for these
rules be their own PR separate to the other `pydocstyle` rules.

Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-26 14:44:46 +00:00
David Szotten
d00559e42a format StmtWith (#5350) 2023-06-26 15:09:06 +01:00
Micha Reiser
49cabca3e7 Format implicit string continuation (#5328) 2023-06-26 12:41:47 +00:00
Micha Reiser
313711aaf9 Prefer the configured quote style
<!--
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 extends the string formatting to respect the configured quote style.

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

## Test Plan

Extended the string test with new cases and set it up to run twice: Once with the `quote_style: Doube`, and once with `quote_style: Single` single and double quotes. 

<!-- How was it tested? -->
2023-06-26 14:24:25 +02:00
Micha Reiser
f18a1f70de Add tests for skip magic trailing comma
<!--
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 adds tests that verify that the magic trailing comma is not respected if disabled in the formatter options. 

Our test setup now allows to create a `<fixture-name>.options.json` file that contains an array of configurations that should be tested. 

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

## Test Plan

It's all about tests :) 

<!-- How was it tested? -->
2023-06-26 14:15:55 +02:00
Micha Reiser
dd0d1afb66 Create PyFormatOptions
<!--
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 adds a new `PyFormatOptions` struct that stores the python formatter options. 
The new options aren't used yet, with the exception of magical trailing commas and the options passed to the printer. 
I'll follow up with more PRs that use the new options (e.g. `QuoteStyle`).

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

## Test Plan

`cargo test` I'll follow up with a new PR that adds support for overriding the options in our fixture tests.
2023-06-26 14:02:17 +02:00
konstin
a52cd47c7f Fix attribute chain own line comments (#5340)
## Motation

Previously,
```python
x = (
    a1
    .a2
    # a
    .  # b
    # c
    a3
)
```
got formatted as
```python
x = a1.a2
# a
.  # b
# c
a3
```
which is invalid syntax. This fixes that.

## Summary

This implements a basic form of attribute chaining
(<https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains>)
by checking if any inner attribute access contains an own line comment,
and if this is the case, adds parentheses around the outermost attribute
access while disabling parentheses for all inner attribute expressions.
We want to replace this with an implementation that uses recursion or a
stack while formatting instead of in `needs_parentheses` and also
includes calls rather sooner than later, but i'm fixing this now because
i'm uncomfortable with having known invalid syntax generation in the
formatter.

## Test Plan

I added new fixtures.
2023-06-26 09:13:07 +00:00
Micha Reiser
8879927b9a Use insta::glob instead of fixture macro (#5364) 2023-06-26 08:46:18 +00:00
Charlie Marsh
dce6a046b0 Add tests for escape-sequence-in-docstring (#5362)
## Summary

Looks like I added a regression in #5360. This PR fixes it and adds
dedicated tests to avoid it in the future.
2023-06-25 22:42:12 -04:00
Charlie Marsh
18c73c1f9b Improve backslash-detection rule for docstrings (#5360) 2023-06-26 01:58:20 +00:00
Charlie Marsh
19c221a2d2 Use matches for os-error-alias (#5361) 2023-06-26 01:57:52 +00:00
Tom Kuson
fd0c3faa70 Add documentation to rules that check docstring quotes (D3XX) (#5351)
## Summary

Add documentation to the `D3XX` rules that check for issues with
docstring quotes. Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-25 22:34:03 +00:00
Charlie Marsh
1fe4073b56 Update the invalid-escape-sequence rule (#5359)
Just a couple small tweaks based on reading the rule with fresh eyes and
new best-practices.
2023-06-25 22:20:31 +00:00
Charlie Marsh
b233763156 Run cargo update (#5357) 2023-06-25 18:16:59 -04:00
Charlie Marsh
1ef4eee089 Add space when migrating to raw string (#5358)
## Summary

We had to do this for f-strings too -- if we add a prefix to `"foo"` in
`return"foo"`, we also need to add a leading space.
2023-06-25 18:10:08 -04:00
Shantanu
0ce38b650e Change W605 autofix to use raw strings if possible (#5352)
Fixes #5061.
2023-06-25 17:35:07 -04:00
Evan Rittenhouse
e0a507e48e Add Applicability to flake8_simplify (#5348) 2023-06-23 22:54:43 +00:00
Dhruv Manilawala
adf5cb5ff7 Ignore type aliases for RUF013 (#5344)
## Summary

Ignore type aliases for RUF013 to avoid flagging false positives:

```python
from typing import Optional

MaybeInt = Optional[int]


def f(arg: MaybeInt = None):
    pass
```

But, at the expense of having false negatives:

```python
Text = str | bytes


def f(arg: Text = None):
    pass
```

## Test Plan

`cargo test`

fixes: #5295
2023-06-23 22:51:09 +00:00
Micha Reiser
d3d69a031e Add JoinCommaSeparatedBuilder (#5342) 2023-06-23 22:03:05 +01:00
Micha Reiser
6ba9d5d5a4 Upgrade RustPython (#5334) 2023-06-23 20:39:47 +00:00
Charlie Marsh
f45d1c2b84 Remove HashMap and HashSet for known-standard-library detection (#5345)
## Summary

This is a lot more concise and probably much more performant (with fewer
instructions).
2023-06-23 19:59:03 +00:00
konstin
4b65446de6 Refactor magic trailing comma (#5339)
## Summary

This is small refactoring to reuse the code that detects the magic
trailing comma across functions. I make this change now to avoid copying
code in a later PR. @MichaReiser is planning on making a larger
refactoring later that integrates with the join nodes builder

## Test Plan

No functional changes. The magic trailing comma behaviour is checked by
the fixtures.
2023-06-23 18:53:55 +02:00
Micha Reiser
cb580f960f Make small tweaks to the profiling documentation (#5335) 2023-06-23 18:11:41 +02:00
James Berry
f85eb709e2 Visit AugAssign target after value (#5325)
## Summary

When visiting AugAssign in evaluation order, the AugAssign `target`
should be visited after it's `value`. Based on my testing, the pseudo
code for `a += b` is effectively:
```python
tmp = a
a = tmp.__iadd__(b)
```

That is, an ideal traversal order would look something like this:
1. load a
2. b
3. op
4. store a

But, there is only a single AST node which captures `a` in the statement
`a += b`, so it cannot be traversed both before and after the traversal
of `b` and the `op`.

Nonetheless, I think traversing `a` after `b` and the `op` makes the
most sense for a number of reasons:
1. All the other assignment expressions traverse their `value`s before
their `target`s. Having `AugAssign` traverse in the same order would be
more consistent.
2. Within the AST, the `ctx` of the `target` for an `AugAssign` is
`Store` (though technically this is a `Load` and `Store` operation, the
AST only indicates it as a `Store`). Since the the store portion of the
`AugAssign` occurs last, I think it makes sense to traverse the `target`
last as well.

The effect of this is marginal, but it may have an impact on the
behavior of #5271.
2023-06-23 09:54:54 -04:00
Charlie Marsh
2f03159c8b Use SSH clones in update_schemastore.py (#5322) 2023-06-23 09:50:10 -04:00
Thomas de Zeeuw
1c638264b2 Keep track of when files are last seen in the cache (#5214)
## Summary

And remove cached files that we haven't seen for a certain period of
time, currently 30 days.

For the last seen timestamp we actually use an `u64`, it's smaller on
disk than `SystemTime` (which size is OS dependent) and fits in an
`AtomicU64` which we can use to update it without locks.

## Test Plan

Added a new unit test, run by `cargo test`.
2023-06-23 15:40:35 +02:00
Micha Reiser
2dfa6ff58d Fix unstable set comprehension formatting (#5327) 2023-06-23 11:50:24 +02:00
konstin
930f03de98 Don't mistake a following if for an elif (#5296)
In the following code, the comment used to get wrongly associated with
the `if False` since it looked like an elif. This fixes it by checking
the indentation and adding a regression test
```python
if True:
    pass
else:  # Comment
    if False:
        pass
    pass
```
    
Originally found in
1570b94a02/gradio/external.py (L478)
2023-06-23 10:07:28 +02:00
Micha Reiser
c52aa8f065 Basic string formatting
<!--
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 implements formatting for non-f-string Strings that do not use implicit concatenation. 

Docstring formatting is out of the scope of this PR.

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

## Test Plan

I added a few tests for simple string literals. 

## Performance

Ouch. This is hitting performance somewhat hard. This is probably because we now iterate each string a couple of times:

1. To detect if it is an implicit string continuation
2. To detect if the string contains any new lines
3. To detect the preferred quote
4. To normalize the string

Edit: I integrated the detection of newlines into the preferred quote detection so that we only iterate the string three time.
We can probably do better by merging the implicit string continuation with the quote detection and new line detection by iterating till the end of the string part and returning the offset. We then use our simple tokenizer to skip over any comments or whitespace until we find the first non trivia token. From there we keep continue doing this in a loop until we reach the end o the string. I'll leave this improvement for later.
2023-06-23 09:46:05 +02:00
Micha Reiser
3e12bdff45 Format Compare Op
<!--
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 adds basic formatting for compare operations.

The implementation currently breaks diffeently when nesting binary like expressions. I haven't yet figured out what Black's logic is in that case but I think that this by itself is already an improvement worth merging.

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

## Test Plan

I added a few new tests 

<!-- How was it tested? -->
2023-06-23 09:35:29 +02:00
James Berry
2142bf6141 Fix annotation and format spec visitors (#5324)
## Summary

The `Visitor` and `preorder::Visitor` traits provide some convenience
functions, `visit_annotation` and `visit_format_spec`, for handling
annotation and format spec expressions respectively. Both of these
functions accept an `&Expr` and have a default implementation which
delegates to `walk_expr`. The problem with this approach is that any
custom handling done in `visit_expr` will be skipped for annotations and
format specs. Instead, to capture any custom logic implemented in
`visit_expr`, both of these function's default implementations should
delegate to `visit_expr` instead of `walk_expr`.

## Example

Consider the below `Visitor` implementation:
```rust
impl<'a> Visitor<'a> for Example<'a> {
    fn visit_expr(&mut self, expr: &'a Expr) {
        match expr {
            Expr::Name(ExprName { id, .. }) => println!("Visiting {:?}", id),
            _ => walk_expr(self, expr),
        }
    }
}
```

Run on the following Python snippet:
```python
a: b
```

I would expect such a visitor to print the following:
```
Visiting b
Visiting a
```

But it instead prints the following:
```
Visiting a
```

Our custom `visit_expr` handler is not invoked for the annotation.

## Test Plan

Tests added in #5271 caught this behavior.
2023-06-23 03:55:42 +00:00
Tom Kuson
1cf307c34c Fix collection-literal-concatenation documentation (#5320)
## Summary

Move `collection-literal-concatenation` markdown documentation to the
correct place.

Fixes error in #5262.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-22 18:37:54 -04:00
Charlie Marsh
7819b95d7f Avoid syntax errors when removing f-string prefixes (#5319)
Closes https://github.com/astral-sh/ruff/issues/5281.

Closes https://github.com/astral-sh/ruff/issues/4827.
2023-06-22 17:21:09 -04:00
Lukas Mayrhofer
4a81cfc51a Allow @Author format for "Missing Author" rule in flake8-todos (#4903)
## Summary

The TD-002 rule "Missing Author" was updated to allow another format
using "@". This reflects the current 0.3.0 version of flake8-todos.
2023-06-22 20:53:58 +00:00
qdegraaf
38e618cd18 [perflint] Add PERF101 with autofix (#5121)
## Summary

Adds PERF101 which checks for unnecessary casts to `list` in for loops. 

NOTE: Is not fully equal to its upstream implementation as this
implementation does not flag based on type annotations
(i.e.):
```python
def foo(x: List[str]):
    for y in list(x):
        ...
```

With the current set-up it's quite hard to get the annotation from a
function arg from its binding. Problem is best considered broader than
this implementation.

## Test Plan

Added fixture. 

## Issue links

Refers: https://github.com/astral-sh/ruff/issues/4789

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-22 20:44:26 +00:00
Charlie Marsh
50f0edd2cb Add dark- and light-mode image modifiers for custom MkDocs themes (#5318)
## Summary

Roughly following the docs
[here](https://squidfunk.github.io/mkdocs-material/reference/images/#custom-light-scheme).

Closes #5311.
2023-06-22 16:11:38 -04:00
Edgar R. M
e0e1d13d9f Fix diagnostics variable name in add_plugin.py script (#5317)
## Summary

Fix a variable name in the `add_plugin.py` script.

## Test Plan

I don't think there are any tests for the scripts, other than manual
confirmation
2023-06-22 20:06:47 +00:00
Charlie Marsh
8bc7378002 Add PythonVersion::Py312 (#5316)
Closes #5310.
2023-06-22 20:01:07 +00:00
Charlie Marsh
cdbd0bd5cd Respect abc decorators when classifying function types (#5315)
Closes #5307.
2023-06-22 19:52:36 +00:00
Charlie Marsh
5f88ff8a96 Allow __slots__ assignments in mutable-class-default (#5314)
Closes #5309.
2023-06-22 19:40:54 +00:00
Charlie Marsh
1c2be54b4a Support pydantic.BaseSettings in mutable-class-default (#5312)
Closes #5308.
2023-06-22 19:27:05 +00:00
Charlie Marsh
5dd00b19e6 Remove off-palette colors from code (#5305) 2023-06-22 16:31:22 +00:00
Charlie Marsh
c0f93fcf3e Publish GitHub release as draft (#5304)
I accidentally changed `draft: false` to `draft: true` in #5240. I
actually think Copilot did this without me realizing.
2023-06-22 16:11:43 +00:00
Charlie Marsh
3238a6ef1f Fix 'our' to 'your' typo (#5303) 2023-06-22 15:58:24 +00:00
Charlie Marsh
96ecfae1c5 Remove off-palette colors (#5302) 2023-06-22 15:52:03 +00:00
konstin
03694ef649 More stability checker options (#5299)
## Summary

This contains three changes:
* repos in `check_ecosystem.py` are stored as `org:name` instead of
`org/name` to create a flat directory layout
* `check_ecosystem.py` performs a maximum of 50 parallel jobs at the
same time to avoid consuming to much RAM
* `check-formatter-stability` gets a new option `--multi-project` so
it's possible to do `cargo run --bin ruff_dev --
check-formatter-stability --multi-project target/checkouts`
With these three changes it becomes easy to check the formatter
stability over a larger number of repositories. This is part of the
integration of integrating formatter regressions checks into the
ecosystem checks.

## Test Plan

```shell
python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true)
cargo run --bin ruff_dev -- check-formatter-stability --multi-project target/checkouts
```
2023-06-22 15:48:11 +00:00
Charlie Marsh
f9f0cf7524 Use __future__ imports in scripts (#5301) 2023-06-22 11:40:16 -04:00
Tom Kuson
eaa10ad2d9 Fix deprecated-import false positives (#5291)
## Summary

Remove recommendations to replace
`typing_extensions.dataclass_transform` and
`typing_extensions.SupportsIndex` with their `typing` library
counterparts.

Closes #5112.

## Test Plan

Added extra checks to the test fixture.

`cargo test`
2023-06-22 15:34:44 +00:00
Evan Rittenhouse
84259f5440 Add Applicability to pycodestyle (#5282) 2023-06-22 11:25:20 -04:00
trag1c
e8ebe0a425 Update docs to match updated logo and color palette (#5283)
![8511](https://github.com/astral-sh/ruff/assets/77130613/862d151f-ff1d-4da8-9230-8dd32f41f197)

## Summary

Supersedes #5277, includes redesigned dark mode.

## Test Plan

* `python scripts/generate_mkdocs.py`
* `mkdocs serve`
2023-06-22 11:19:34 -04:00
konstin
d407165aa7 Fix formatter panic with comment after parenthesized dict value (#5293)
## Summary

This snippet used to panic because it expected to see a comma or
something similar after the `2` but met the closing parentheses that is
not part of the range and panicked
```python
a = {
    1: (2),
    # comment
    3: True,
}
```

Originally found in
636a717ef0/testing/marionette/client/marionette_driver/geckoinstance.py (L109)

This snippet is also the test plan.
2023-06-22 16:52:48 +02:00
Micha Reiser
f7e1cf4b51 Format class definitions (#5289) 2023-06-22 09:09:43 +00:00
konstin
7d4f8e59da Improve FormatExprCall dummy (#5290)
This solves an instability when formatting cpython. It also introduces
another one, but i think it's still a worthwhile change for now.

There's no proper testing since this is just a dummy.
2023-06-22 10:59:30 +02:00
Charlie Marsh
2c63f8cdea Update reference to release step (#5280) 2023-06-22 02:04:43 +00:00
Charlie Marsh
1c0a3a467f Bump version to 0.0.275 (#5276) 2023-06-21 21:53:37 -04:00
Charlie Marsh
6b8b318d6b Use mod tests consistently (#5278)
As per the Rust documentation.
2023-06-22 01:50:28 +00:00
Charlie Marsh
c0c59b82ec Use 'Checks for uses' consistently (#5279) 2023-06-22 01:44:52 +00:00
700 changed files with 33028 additions and 9932 deletions

View File

@@ -491,16 +491,16 @@ jobs:
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:
draft: false
draft: true
files: binaries/*
tag_name: v${{ inputs.tag }}
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Release
name: Update dependents
runs-on: ubuntu-latest
needs: release
needs: publish-release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v6

View File

@@ -6,7 +6,9 @@ exclude: |
crates/ruff/src/rules/.*/snapshots/.*|
crates/ruff_cli/resources/.*|
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/src/snapshots/.*
crates/ruff_python_formatter/tests/snapshots/.*|
crates/ruff_python_resolver/resources/.*|
crates/ruff_python_resolver/tests/snapshots/.*
)$
repos:

View File

@@ -1,5 +1,31 @@
# Breaking Changes
## 0.0.276
### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470))
The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was
removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring
the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism.
Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as
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.
- 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.
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
like Pydantic and FastAPI.
Note that this is not a breaking change, but is included here to complement the previous removal
of `keep-runtime-typing`.
## 0.0.268
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427))

View File

@@ -327,22 +327,18 @@ git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resour
To benchmark the release build:
```shell
cargo build --release && hyperfine --ignore-failure --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache" \
"./target/release/ruff ./crates/ruff/resources/test/cpython/"
cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e" \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ -e"
Benchmark 1: ./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache
Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms]
Range (min … max): 289.9 ms … 301.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./crates/ruff/resources/test/cpython/
Time (mean ± σ): 48.0 ms ± 3.1 ms [User: 65.2 ms, System: 124.7 ms]
Range (min … max): 45.0 ms … 66.7 ms 62 runs
Warning: Ignoring non-zero exit code.
Summary
'./target/release/ruff ./crates/ruff/resources/test/cpython/' ran
6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache'
@@ -503,7 +499,7 @@ examples.
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to

435
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.28.0" }
insta = { version = "1.30.0" }
is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
@@ -49,16 +49,15 @@ toml = { version = "0.7.2" }
# v0.0.1
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
# v0.0.3
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" }
# v0.0.3
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
# v0.0.3
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false, features = ["num-bigint"] }
# v0.0.3
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false }
# v0.0.3
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
# Please tag the RustPython version everytime 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"] }
[profile.release]
lto = "fat"

26
LICENSE
View File

@@ -1224,6 +1224,32 @@ are:
SOFTWARE.
"""
- Pyright, licensed as follows:
"""
MIT License
Pyright - A static type checker for the Python language
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
"""
- rust-analyzer/text-size, licensed under the MIT license:
"""
Permission is hereby granted, free of charge, to any

View File

@@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.274
rev: v0.0.276
hooks:
- id: ruff
```
@@ -330,9 +330,11 @@ We're grateful to the maintainers of these tools for their work, and for all
the value they've provided to the Python community.
Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter),
and again draws on both the APIs and implementation details of [Rome](https://github.com/rome/tools),
and again draws on both API and implementation details from [Rome](https://github.com/rome/tools),
[Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black).
Ruff's import resolver is based on the import resolution algorithm from [Pyright](https://github.com/microsoft/pyright).
Ruff is also influenced by a number of tools outside the Python ecosystem, like
[Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint).

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.274"
version = "0.0.276"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -42,6 +42,7 @@ is-macro = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
nohash-hasher = { workspace = true }
num-bigint = { workspace = true }
@@ -87,4 +88,3 @@ colored = { workspace = true, features = ["no-color"] }
[features]
default = []
schemars = ["dep:schemars"]
jupyter_notebook = []

View File

@@ -23,6 +23,10 @@ class Foobar(unittest.TestCase):
with self.assertRaises(Exception):
raise Exception("Evil I say!")
def also_evil_raises(self) -> None:
with self.assertRaises(BaseException):
raise Exception("Evil I say!")
def context_manager_raises(self) -> None:
with self.assertRaises(Exception) as ex:
raise Exception("Context manager is good")
@@ -41,6 +45,9 @@ def test_pytest_raises():
with pytest.raises(Exception):
raise ValueError("Hello")
with pytest.raises(Exception), pytest.raises(ValueError):
raise ValueError("Hello")
with pytest.raises(Exception, "hello"):
raise ValueError("This is fine")

View File

@@ -111,3 +111,19 @@ class PerfectlyFine(models.Model):
@property
def random_property(self):
return "%s" % self
class MultipleConsecutiveFields(models.Model):
"""Model that contains multiple out-of-order field definitions in a row."""
class Meta:
verbose_name = "test"
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
def get_absolute_url(self):
pass
middle_name = models.CharField(max_length=32)

View File

@@ -0,0 +1,6 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -0,0 +1,6 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -0,0 +1,31 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -0,0 +1,31 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -0,0 +1,15 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@@ -0,0 +1,15 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@@ -0,0 +1,14 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@@ -0,0 +1,14 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@@ -91,3 +91,4 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@@ -98,3 +98,4 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@@ -36,3 +36,11 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg"
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg"
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff"
class Demo:
"""Docstrings are excluded from this rule. Some padding."""
def func() -> None:
"""Docstrings are excluded from this rule. Some padding."""

View File

@@ -28,3 +28,9 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI05
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
class Demo:
"""Docstrings are excluded from this rule. Some padding.""" # OK
def func() -> None:
"""Docstrings are excluded from this rule. Some padding.""" # OK

View File

@@ -1,6 +1,12 @@
# T002 - accepted
# TODO (evanrittenhouse): this has an author
# TODO(evanrittenhouse): this also has an author
# TODO(evanrittenhouse): this has an author
# TODO (evanrittenhouse) and more: this has an author
# TODO(evanrittenhouse) and more: this has an author
# TODO@mayrholu: this has an author
# TODO @mayrholu: this has an author
# TODO@mayrholu and more: this has an author
# TODO @mayrholu and more: this has an author
# T002 - errors
# TODO: this has no author
# FIXME: neither does this

View File

@@ -1,5 +1,6 @@
# Do this (new version)
from numpy.random import default_rng
rng = default_rng()
vals = rng.standard_normal(10)
more_vals = rng.standard_normal(10)
@@ -7,11 +8,13 @@ numbers = rng.integers(high, size=5)
# instead of this (legacy version)
from numpy import random
vals = random.standard_normal(10)
more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5)
import numpy
numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()

View File

@@ -0,0 +1,15 @@
import numpy as np
np.round_(np.random.rand(5, 5), 2)
np.product(np.random.rand(5, 5))
np.cumproduct(np.random.rand(5, 5))
np.sometrue(np.random.rand(5, 5))
np.alltrue(np.random.rand(5, 5))
from numpy import round_, product, cumproduct, sometrue, alltrue
round_(np.random.rand(5, 5), 2)
product(np.random.rand(5, 5))
cumproduct(np.random.rand(5, 5))
sometrue(np.random.rand(5, 5))
alltrue(np.random.rand(5, 5))

View File

@@ -4,7 +4,9 @@ x = pd.DataFrame()
x.drop(["a"], axis=1, inplace=True)
x.drop(["a"], axis=1, inplace=True)
x.y.drop(["a"], axis=1, inplace=True)
x["y"].drop(["a"], axis=1, inplace=True)
x.drop(
inplace=True,
@@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True)
x.drop(["a"], axis=1, inplace=True, **kwargs)
f(x.drop(["a"], axis=1, inplace=True))
x.apply(lambda x: x.sort_values('a', inplace=True))
x.apply(lambda x: x.sort_values("a", inplace=True))
import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call

View File

@@ -1,4 +1,4 @@
from abc import ABCMeta
import abc
import pydantic
@@ -19,6 +19,10 @@ class Class:
def class_method(cls):
pass
@abc.abstractclassmethod
def abstract_class_method(cls):
pass
@staticmethod
def static_method(x):
return x
@@ -41,7 +45,7 @@ class Class:
...
class MetaClass(ABCMeta):
class MetaClass(abc.ABCMeta):
def bad_method(self):
pass

View File

@@ -1,4 +1,4 @@
from abc import ABCMeta
import abc
import pydantic
@@ -34,6 +34,23 @@ class Class:
def stillBad(cls, my_field: str) -> str:
pass
@classmethod
def badAllowed(cls):
pass
@classmethod
def stillBad(cls):
pass
@abc.abstractclassmethod
def badAllowed(cls):
pass
@abc.abstractclassmethod
def stillBad(cls):
pass
class PosOnlyClass:
def badAllowed(this, blah, /, self, something: str):
pass

View File

@@ -0,0 +1,52 @@
foo_tuple = (1, 2, 3)
foo_list = [1, 2, 3]
foo_set = {1, 2, 3}
foo_dict = {1: 2, 3: 4}
foo_int = 123
for i in list(foo_tuple): # PERF101
pass
for i in list(foo_list): # PERF101
pass
for i in list(foo_set): # PERF101
pass
for i in list((1, 2, 3)): # PERF101
pass
for i in list([1, 2, 3]): # PERF101
pass
for i in list({1, 2, 3}): # PERF101
pass
for i in list(
{
1,
2,
3,
}
):
pass
for i in list( # Comment
{1, 2, 3}
): # PERF101
pass
for i in list(foo_dict): # Ok
pass
for i in list(1): # Ok
pass
for i in list(foo_int): # Ok
pass
import itertools
for i in itertools.product(foo_int): # Ok
pass

View File

@@ -0,0 +1,28 @@
for i in range(10):
try: # PERF203
print(f"{i}")
except:
print("error")
try:
for i in range(10):
print(f"{i}")
except:
print("error")
i = 0
while i < 10: # PERF203
try:
print(f"{i}")
except:
print("error")
i += 1
try:
i = 0
while i < 10:
print(f"{i}")
i += 1
except:
print("error")

View File

@@ -0,0 +1,32 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
elif i % 2:
result.append(i) # PERF401
else:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # OK

View File

@@ -0,0 +1,19 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.insert(0, i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # OK

View File

@@ -1,51 +1,135 @@
#: E731
f = lambda x: 2 * x
#: E731
f = lambda x: 2 * x
#: E731
while False:
this = lambda y, z: 2 * x
#: E731
f = lambda: (yield 1)
#: E731
f = lambda: (yield from g())
#: E731
class F:
def scope():
# E731
f = lambda x: 2 * x
f = object()
f.method = lambda: "Method"
f = {}
f["a"] = lambda x: x**2
f = []
f.append(lambda x: x**2)
f = g = lambda x: x**2
lambda: "no-op"
def scope():
# E731
f = lambda x: 2 * x
# Annotated
from typing import Callable, ParamSpec
P = ParamSpec("P")
def scope():
# E731
while False:
this = lambda y, z: 2 * x
def scope():
# E731
f = lambda: (yield 1)
def scope():
# E731
f = lambda: (yield from g())
def scope():
# OK
f = object()
f.method = lambda: "Method"
def scope():
# OK
f = {}
f["a"] = lambda x: x**2
def scope():
# OK
f = []
f.append(lambda x: x**2)
def scope():
# OK
f = g = lambda x: x**2
def scope():
# OK
lambda: "no-op"
class Scope:
# E731
f = lambda x: 2 * x
class Scope:
from typing import Callable
# E731
f: Callable[[int], int] = lambda x: 2 * x
def scope():
# E731
from typing import Callable
x: Callable[[int], int]
if True:
x = lambda: 1
else:
x = lambda: 2
return x
def scope():
# E731
from typing import Callable, ParamSpec
# ParamSpec cannot be used in this context, so do not preserve the annotation.
P = ParamSpec("P")
f: Callable[P, int] = lambda *args: len(args)
def scope():
# E731
from typing import Callable
f: Callable[[], None] = lambda: None
def scope():
# E731
from typing import Callable
f: Callable[..., None] = lambda a, b: None
def scope():
# E731
from typing import Callable
f: Callable[[int], int] = lambda x: 2 * x
# ParamSpec cannot be used in this context, so do not preserve the annotation.
f: Callable[P, int] = lambda *args: len(args)
f: Callable[[], None] = lambda: None
f: Callable[..., None] = lambda a, b: None
f: Callable[[int], int] = lambda x: 2 * x
# Let's use the `Callable` type from `collections.abc` instead.
from collections.abc import Callable
def scope():
# E731
f: Callable[[str, int], str] = lambda a, b: a * b
f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
from collections.abc import Callable
f: Callable[[str, int], str] = lambda a, b: a * b
# Override `Callable`
class Callable:
pass
def scope():
# E731
from collections.abc import Callable
f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
# Do not copy the annotation from here on out.
f: Callable[[str, int], str] = lambda a, b: a * b
def scope():
# E731
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]

View File

@@ -19,6 +19,14 @@ with \_ somewhere
in the middle
"""
#: W605:1:38
value = 'new line\nand invalid escape \_ here'
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'

View File

@@ -19,6 +19,11 @@ with \_ somewhere
in the middle
"""
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'

View File

@@ -0,0 +1,29 @@
def double_quotes_backslash():
"""Sum\\mary."""
def double_quotes_backslash_raw():
r"""Sum\mary."""
def double_quotes_backslash_uppercase():
R"""Sum\\mary."""
def make_unique_pod_id(pod_id: str) -> str | None:
r"""
Generate a unique Pod name.
Kubernetes pod names must consist of one or more lowercase
rfc1035/rfc1123 labels separated by '.' with a maximum length of 253
characters.
Name must pass the following regex for validation
``^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$``
For more details, see:
https://github.com/kubernetes/kubernetes/blob/release-1.1/docs/design/identifiers.md
:param pod_id: requested pod name
:return: ``str`` valid Pod name of appropriate length
"""

View File

@@ -0,0 +1,25 @@
def f(a: int, b: int) -> int:
"""Showcase function.
Parameters
----------
a : int
_description_
b : int
_description_
Returns
-------
int
_description
"""
return b - a
def f() -> int:
"""Showcase function.
Parameters
----------
Returns
-------
"""

View File

@@ -513,3 +513,19 @@ def implicit_string_concatenation():
A value of some sort.
""""Extra content"
def replace_equals_with_dash():
"""Equal length equals should be replaced with dashes.
Parameters
==========
"""
def replace_equals_with_dash2():
"""Here, the length of equals is not the same.
Parameters
===========
"""

View File

@@ -37,7 +37,10 @@ f"{{test}}"
f'{{ 40 }}'
f"{{a {{x}}"
f"{{{{x}}}}"
""f""
''f""
(""f""r"")
# To be fixed
# Error: f-string: single '}' is not allowed at line 41 column 8
# f"\{{x}}"
# f"\{{x}}"

View File

@@ -36,3 +36,6 @@ for item in set(("apples", "lemons", "water")): # set constructor is fine
for number in {i for i in range(10)}: # set comprehensions are fine
print(number)
for item in {*numbers_set, 4, 5, 6}: # set unpacking is fine
print(f"I like {item}.")

View File

@@ -0,0 +1,35 @@
# Errors.
class Foo:
__slots__ = "bar"
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: str = "bar"
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: str = f"bar"
def __init__(self, bar):
self.bar = bar
# Non-errors.
class Foo:
__slots__ = ("bar",)
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: tuple[str, ...] = ("bar",)
def __init__(self, bar):
self.bar = bar

View File

@@ -70,3 +70,8 @@ print("foo".encode()) # print(b"foo")
"abc"
"def"
)).encode()
(f"foo{bar}").encode("utf-8")
(f"foo{bar}").encode(encoding="utf-8")
("unicode text©").encode("utf-8")
("unicode text©").encode(encoding="utf-8")

View File

@@ -48,3 +48,12 @@ if True: from collections import (
# OK
from a import b
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import SupportsIndex
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import NamedTuple
# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
from typing_extensions import dataclass_transform

View File

@@ -1,23 +1,14 @@
import typing
from typing import ClassVar, Sequence, Final
KNOWINGLY_MUTABLE_DEFAULT = []
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: typing.ClassVar[list[int]] = []
final_variable: typing.Final[list[int]] = []
__slots__ = {
"mutable_default": "A mutable default value",
}
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
@@ -30,7 +21,6 @@ class C:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
@@ -43,7 +33,5 @@ class D(BaseModel):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -221,3 +221,23 @@ def f(arg: Union["No" "ne", "int"] = None):
# Avoid flagging when there's a parse error in the forward reference
def f(arg: Union["<>", "int"] = None):
pass
# Type aliases
Text = str | bytes
def f(arg: Text = None):
pass
def f(arg: "Text" = None):
pass
from custom_typing import MaybeInt
def f(arg: MaybeInt = None):
pass

View File

@@ -36,6 +36,7 @@ use crate::importer::Importer;
use crate::noqa::NoqaMapping;
use crate::registry::Rule;
use crate::rules::flake8_builtins::helpers::AnyShadowing;
use crate::rules::{
airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
@@ -71,7 +72,7 @@ pub(crate) struct Checker<'a> {
deferred: Deferred<'a>,
pub(crate) diagnostics: Vec<Diagnostic>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>,
}
impl<'a> Checker<'a> {
@@ -358,11 +359,7 @@ where
..
}) => {
if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
self.diagnostics
.extend(flake8_django::rules::non_leading_receiver_decorator(
decorator_list,
|expr| self.semantic.resolve_call_path(expr),
));
flake8_django::rules::non_leading_receiver_decorator(self, decorator_list);
}
if self.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) =
@@ -504,8 +501,7 @@ where
}
}
if self.enabled(Rule::HardcodedPasswordDefault) {
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_default(args));
flake8_bandit::rules::hardcoded_password_default(self, args);
}
if self.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(self, stmt, decorator_list, args);
@@ -643,10 +639,7 @@ where
},
) => {
if self.enabled(Rule::DjangoNullableModelStringField) {
self.diagnostics
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
flake8_django::rules::nullable_model_string_field(self, body);
}
if self.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) =
@@ -667,21 +660,17 @@ where
}
if !self.is_stub {
if self.enabled(Rule::DjangoModelWithoutDunderStr) {
if let Some(diagnostic) =
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
flake8_django::rules::model_without_dunder_str(self, class_def);
}
}
if self.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name);
}
if self.enabled(Rule::UselessObjectInheritance) {
pyupgrade::rules::useless_object_inheritance(self, class_def, stmt);
pyupgrade::rules::useless_object_inheritance(self, class_def);
}
if self.enabled(Rule::UnnecessaryClassParentheses) {
pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt);
pyupgrade::rules::unnecessary_class_parentheses(self, class_def);
}
if self.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) =
@@ -770,6 +759,9 @@ where
if self.enabled(Rule::NoSlotsInNamedtupleSubclass) {
flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def);
}
if self.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(self, class_def);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if self.enabled(Rule::MultipleImportsOnOneLine) {
@@ -1367,6 +1359,51 @@ where
self.diagnostics.push(diagnostic);
}
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
Rule::WrongTupleLengthVersionComparison,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_version_info(self, value);
}
} else {
flake8_pyi::rules::unrecognized_version_info(self, test);
}
}
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_platform(self, value);
}
} else {
flake8_pyi::rules::unrecognized_platform(self, test);
}
}
if self.enabled(Rule::BadVersionInfoComparison) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::bad_version_info_comparison(self, value);
}
} else {
flake8_pyi::rules::bad_version_info_comparison(self, test);
}
}
if self.enabled(Rule::ComplexIfStatementInStub) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::complex_if_statement_in_stub(self, value);
}
} else {
flake8_pyi::rules::complex_if_statement_in_stub(self, test);
}
}
}
}
Stmt::Assert(ast::StmtAssert {
test,
@@ -1406,7 +1443,7 @@ where
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if self.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(self, stmt, items);
flake8_bugbear::rules::assert_raises_exception(self, items);
}
if self.enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(self, stmt, items, body);
@@ -1430,6 +1467,9 @@ where
if self.enabled(Rule::UselessElseOnLoop) {
pylint::rules::useless_else_on_loop(self, stmt, body, orelse);
}
if self.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(self, body);
}
}
Stmt::For(ast::StmtFor {
target,
@@ -1477,10 +1517,22 @@ where
if self.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(self, target, iter);
}
if self.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(self, body);
}
}
if self.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(self, target, iter);
}
if self.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(self, target, body);
}
if self.enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(self, target, body);
}
if self.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(self, iter);
}
}
Stmt::Try(ast::StmtTry {
body,
@@ -1516,9 +1568,7 @@ where
pyupgrade::rules::os_error_alias_handlers(self, handlers);
}
if self.enabled(Rule::PytestAssertInExcept) {
self.diagnostics.extend(
flake8_pytest_style::rules::assert_in_exception_handler(handlers),
);
flake8_pytest_style::rules::assert_in_exception_handler(self, handlers);
}
if self.enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception(
@@ -1559,11 +1609,7 @@ where
flake8_bugbear::rules::assignment_to_os_environ(self, targets);
}
if self.enabled(Rule::HardcodedPasswordString) {
if let Some(diagnostic) =
flake8_bandit::rules::assign_hardcoded_password_string(value, targets)
{
self.diagnostics.push(diagnostic);
}
flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets);
}
if self.enabled(Rule::GlobalStatement) {
for target in targets.iter() {
@@ -2095,6 +2141,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, value,
@@ -2105,7 +2152,8 @@ where
if self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep604_annotation(
self, expr, slice, operator,
@@ -2181,6 +2229,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.is_stub {
if self.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(self, expr);
@@ -2200,6 +2251,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@@ -2210,7 +2262,8 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep585_annotation(
self,
@@ -2275,6 +2328,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@@ -2285,7 +2339,8 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep585_annotation(self, expr, &replacement);
}
@@ -2303,6 +2358,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_attribute(self, expr);
}
@@ -2546,8 +2604,7 @@ where
flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords);
}
if self.enabled(Rule::HardcodedPasswordFuncArg) {
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
flake8_bandit::rules::hardcoded_password_func_arg(self, keywords);
}
if self.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
@@ -2680,17 +2737,12 @@ where
flake8_debugger::rules::debugger_call(self, expr, func);
}
if self.enabled(Rule::PandasUseOfInplaceArgument) {
self.diagnostics.extend(
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords)
.into_iter(),
);
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords);
}
pandas_vet::rules::call(self, func);
if self.enabled(Rule::PandasUseOfPdMerge) {
if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) {
self.diagnostics.push(diagnostic);
};
pandas_vet::rules::use_of_pd_merge(self, func);
}
if self.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo(
@@ -2807,16 +2859,13 @@ where
&self.settings.flake8_gettext.functions_names,
) {
if self.enabled(Rule::FStringInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::f_string_in_gettext_func_call(args));
flake8_gettext::rules::f_string_in_gettext_func_call(self, args);
}
if self.enabled(Rule::FormatInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::format_in_gettext_func_call(args));
flake8_gettext::rules::format_in_gettext_func_call(self, args);
}
if self.enabled(Rule::PrintfInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::printf_in_gettext_func_call(args));
flake8_gettext::rules::printf_in_gettext_func_call(self, args);
}
}
if self.enabled(Rule::UncapitalizedEnvironmentVariables) {
@@ -2857,7 +2906,7 @@ where
flake8_use_pathlib::rules::replaceable_by_pathlib(self, func);
}
if self.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::numpy_legacy_random(self, func);
numpy::rules::legacy_random(self, func);
}
if self.any_enabled(&[
Rule::LoggingStringFormat,
@@ -3157,11 +3206,10 @@ where
flake8_2020::rules::compare(self, left, ops, comparators);
}
if self.enabled(Rule::HardcodedPasswordString) {
self.diagnostics.extend(
flake8_bandit::rules::compare_to_hardcoded_password_string(
left,
comparators,
),
flake8_bandit::rules::compare_to_hardcoded_password_string(
self,
left,
comparators,
);
}
if self.enabled(Rule::ComparisonWithItself) {
@@ -3182,29 +3230,6 @@ where
if self.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
flake8_pyi::rules::unrecognized_platform(
self,
expr,
left,
ops,
comparators,
);
}
if self.enabled(Rule::BadVersionInfoComparison) {
flake8_pyi::rules::bad_version_info_comparison(
self,
expr,
left,
ops,
comparators,
);
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
@@ -4141,88 +4166,6 @@ impl<'a> Checker<'a> {
// Create the `Binding`.
let binding_id = self.semantic.push_binding(range, kind, flags);
let binding = self.semantic.binding(binding_id);
// Determine whether the binding shadows any existing bindings.
if let Some((stack_index, shadowed_id)) = self
.semantic
.scopes
.ancestors(self.semantic.scope_id)
.enumerate()
.find_map(|(stack_index, scope)| {
scope.get(name).and_then(|binding_id| {
let binding = self.semantic.binding(binding_id);
if binding.is_unbound() {
None
} else {
Some((stack_index, binding_id))
}
})
})
{
let shadowed = self.semantic.binding(shadowed_id);
let in_current_scope = stack_index == 0;
if !shadowed.kind.is_builtin()
&& shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
!branch_detection::different_forks(left, right, &self.semantic.stmts)
})
})
{
let shadows_import = matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
);
if binding.kind.is_loop_var() && shadows_import {
if self.enabled(Rule::ImportShadowedByLoopVar) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
} else if in_current_scope {
if !shadowed.is_used()
&& binding.redefines(shadowed)
&& (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import)
&& !(shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(self.semantic.stmts[shadowed.source.unwrap()]),
&self.semantic,
))
{
if self.enabled(Rule::RedefinedWhileUnused) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: name.to_string(),
line,
},
binding.range,
);
if let Some(range) = binding.parent_range(&self.semantic) {
diagnostic.set_parent(range.start());
}
self.diagnostics.push(diagnostic);
}
}
} else if shadows_import && binding.redefines(shadowed) {
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
}
}
// If there's an existing binding in this scope, copy its references.
if let Some(shadowed_id) = self.semantic.scopes[scope_id].get(name) {
@@ -4256,6 +4199,21 @@ impl<'a> Checker<'a> {
self.semantic.bindings[binding_id].references = references;
}
} else if let Some(shadowed_id) = self
.semantic
.scopes
.ancestors(scope_id)
.skip(1)
.find_map(|scope| scope.get(name))
{
// Otherwise, if there's an existing binding in a parent scope, mark it as shadowed.
let binding = self.semantic.binding(binding_id);
let shadowed = self.semantic.binding(shadowed_id);
if binding.redefines(shadowed) {
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
}
// Add the binding to the scope.
@@ -4274,7 +4232,7 @@ impl<'a> Checker<'a> {
{
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
let scope = self.semantic.scope_mut();
let scope = self.semantic.global_scope_mut();
scope.add(builtin, binding_id);
}
}
@@ -4477,7 +4435,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_delete(&mut self, expr: &'a Expr) {
let Expr::Name(ast::ExprName { id, .. } )= expr else {
let Expr::Name(ast::ExprName { id, .. }) = expr else {
return;
};
@@ -4678,17 +4636,19 @@ impl<'a> Checker<'a> {
fn check_deferred_scopes(&mut self) {
if !self.any_enabled(&[
Rule::UnusedImport,
Rule::GlobalVariableNotAssigned,
Rule::UndefinedLocalWithImportStarUsage,
Rule::ImportShadowedByLoopVar,
Rule::RedefinedWhileUnused,
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyThirdPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::UndefinedExport,
Rule::TypingOnlyThirdPartyImport,
Rule::UnaliasedCollectionsAbcSetImport,
Rule::UnconventionalImportAlias,
Rule::UndefinedExport,
Rule::UndefinedLocalWithImportStarUsage,
Rule::UndefinedLocalWithImportStarUsage,
Rule::UnusedImport,
]) {
return;
}
@@ -4755,8 +4715,8 @@ impl<'a> Checker<'a> {
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope_id in self.deferred.scopes.iter().rev() {
let scope = &self.semantic.scopes[*scope_id];
for scope_id in self.deferred.scopes.iter().rev().copied() {
let scope = &self.semantic.scopes[scope_id];
if scope.kind.is_module() {
// F822
@@ -4815,21 +4775,123 @@ impl<'a> Checker<'a> {
continue;
}
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `shadowed_bindings` if
// the bindings are in different scopes.
if self.enabled(Rule::RedefinedWhileUnused) {
// F402
if self.enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
if let Some(shadowed_id) = self.semantic.shadowed_binding(binding_id) {
let shadowed = self.semantic.binding(shadowed_id);
if shadowed.is_used() {
for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &self.semantic.bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &self.semantic.bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &self.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
let binding = self.semantic.binding(binding_id);
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
}
}
// F811
if self.enabled(Rule::RedefinedWhileUnused) {
for (name, binding_id) in scope.bindings() {
for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &self.semantic.bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &self.semantic.bindings[shadow.shadowed_id()];
if shadowed.is_used() {
continue;
}
// If the shadowing binding isn't considered a "redefinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) && self.settings.dummy_variable_rgx.is_match(name)
{
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
self.semantic.stmts[shadowed.source.unwrap()],
),
&self.semantic,
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &self.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
@@ -4851,7 +4913,7 @@ impl<'a> Checker<'a> {
} else {
self.semantic
.scopes
.ancestor_ids(*scope_id)
.ancestor_ids(scope_id)
.flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter())
.copied()
.collect()

View File

@@ -156,6 +156,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented),
// pylint
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
@@ -595,6 +596,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-pyi
(Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam),
(Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub),
(Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck),
(Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison),
(Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison),
(Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison),
(Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck),
(Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName),
@@ -741,6 +746,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// numpy
(Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias),
(Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom),
(Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction),
// ruff
(Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString),
@@ -784,7 +790,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
// perflint
(Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast),
(Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator),
(Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme),

View File

@@ -18,7 +18,6 @@ pub(crate) struct Docstring<'a> {
pub(crate) expr: &'a Expr,
/// The content of the docstring, including the leading and trailing quotes.
pub(crate) contents: &'a str,
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].
pub(crate) body_range: TextRange,
pub(crate) indentation: &'a str,

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines};
use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
@@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> {
let mut contexts = Vec::new();
let mut last: Option<SectionContextData> = None;
let mut previous_line = None;
for line in contents.universal_newlines() {
if previous_line.is_none() {
// skip the first line
previous_line = Some(line.as_str());
continue;
}
let mut lines = contents.universal_newlines().peekable();
// Skip the first line, which is the summary.
let mut previous_line = lines.next();
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
@@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> {
if is_docstring_section(
&line,
section_name_range,
previous_line.unwrap_or_default(),
previous_line.as_ref(),
lines.peek(),
) {
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), line.start());
@@ -178,7 +177,7 @@ impl<'a> SectionContexts<'a> {
}
}
previous_line = Some(line.as_str());
previous_line = Some(line);
}
if let Some(mut last) = last.take() {
@@ -388,7 +387,13 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool {
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
@@ -396,13 +401,29 @@ fn is_docstring_section(line: &str, section_name_range: TextRange, previous_line
return false;
}
let prev_line = previous_lines.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
// Determine whether the next line is an underline, e.g., "-----".
let next_line_is_underline = next_line.map_or(false, |next_line| {
let next_line = next_line.trim();
if next_line.is_empty() {
false
} else {
let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '='));
next_line_is_underline
}
});
if next_line_is_underline {
return true;
}
// Determine whether the previous line looks like the end of a paragraph.
let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| {
let previous_line = previous_line.trim();
let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| previous_line.ends_with(char));
previous_line_ends_with_punctuation || previous_line.is_empty()
});
if !previous_line_looks_like_end_of_paragraph {
return false;
}

View File

@@ -39,15 +39,6 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
Ok(String::from_utf8(writer)?)
}
/// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`).
pub fn is_jupyter_notebook(path: &Path) -> bool {
path.extension()
.map_or(false, |ext| ext == JUPYTER_NOTEBOOK_EXT)
// For now this is feature gated here, the long term solution depends on
// https://github.com/astral-sh/ruff/issues/3410
&& cfg!(feature = "jupyter_notebook")
}
impl Cell {
/// Return the [`SourceValue`] of the cell.
fn source(&self) -> &SourceValue {
@@ -277,11 +268,12 @@ impl Notebook {
.markers()
.iter()
.rev()
.find(|m| m.source <= *offset) else {
// There are no markers above the current offset, so we can
// stop here.
break;
};
.find(|m| m.source <= *offset)
else {
// There are no markers above the current offset, so we can
// stop here.
break;
};
last_marker = Some(marker);
marker
}
@@ -445,15 +437,13 @@ impl Notebook {
}
#[cfg(test)]
mod test {
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::JupyterIndex;
#[cfg(feature = "jupyter_notebook")]
use crate::jupyter::is_jupyter_notebook;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
@@ -512,16 +502,6 @@ mod test {
Ok(())
}
#[test]
#[cfg(feature = "jupyter_notebook")]
fn inclusions() {
let path = Path::new("foo/bar/baz");
assert!(!is_jupyter_notebook(path));
let path = Path::new("foo/bar/baz.ipynb");
assert!(is_jupyter_notebook(path));
}
#[test]
fn test_concat_notebook() -> Result<()> {
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;

View File

@@ -25,14 +25,12 @@ isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted
isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted
|
2 | import random
3 | import math
4 | / from typing import Any
5 | | import collections
6 | | # Newline should be added here
1 | / from typing import Any
2 | | import collections
3 | | # Newline should be added here
| |_^ I001
7 | def foo():
8 | pass
4 | def foo():
5 | pass
|
= help: Organize imports

View File

@@ -9,6 +9,8 @@ pub use ruff_python_ast::source_code::round_trip;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
mod autofix;
mod checkers;
mod codes;

View File

@@ -149,7 +149,14 @@ impl Display for DisplayGroupedMessage<'_> {
if self.show_source {
use std::fmt::Write;
let mut padded = PadAdapter::new(f);
writeln!(padded, "{}", MessageCodeFrame { message })?;
writeln!(
padded,
"{}",
MessageCodeFrame {
message,
jupyter_index: self.jupyter_index
}
)?;
}
Ok(())

View File

@@ -11,7 +11,7 @@ use ruff_text_size::{TextRange, TextSize};
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use crate::fs::relativize_path;
use crate::jupyter::Notebook;
use crate::jupyter::{JupyterIndex, Notebook};
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
@@ -72,13 +72,13 @@ impl Emitter for TextEmitter {
)?;
let start_location = message.compute_start_location();
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = context
let jupyter_index = context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index)
{
.map(Notebook::index);
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {
write!(
writer,
"cell {cell}{sep}",
@@ -114,7 +114,14 @@ impl Emitter for TextEmitter {
)?;
if self.flags.contains(EmitterFlags::SHOW_SOURCE) {
writeln!(writer, "{}", MessageCodeFrame { message })?;
writeln!(
writer,
"{}",
MessageCodeFrame {
message,
jupyter_index
}
)?;
}
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
@@ -158,6 +165,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for MessageCodeFrame<'_> {
@@ -182,6 +190,20 @@ impl Display for MessageCodeFrame<'_> {
let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);
// If we're working on a jupyter notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_start_cell = jupyter_index
.cell(content_start_index.get())
.unwrap_or_default();
while start_index < content_start_index {
if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
break;
}
start_index = start_index.saturating_add(1);
}
}
// Trim leading empty lines.
while start_index < content_start_index {
if !source_code.line_text(start_index).trim().is_empty() {
@@ -195,7 +217,21 @@ impl Display for MessageCodeFrame<'_> {
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
// Trim trailing empty lines
// If we're working on a jupyter notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_end_cell = jupyter_index
.cell(content_end_index.get())
.unwrap_or_default();
while end_index > content_end_index {
if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
break;
}
end_index = end_index.saturating_sub(1);
}
}
// Trim trailing empty lines.
while end_index > content_end_index {
if !source_code.line_text(end_index).trim().is_empty() {
break;
@@ -224,7 +260,14 @@ impl Display for MessageCodeFrame<'_> {
title: None,
slices: vec![Slice {
source: &source.text,
line_start: start_index.get(),
line_start: self.jupyter_index.map_or_else(
|| start_index.get(),
|jupyter_index| {
jupyter_index
.cell_row(start_index.get())
.unwrap_or_default() as usize
},
),
annotations: vec![SourceAnnotation {
label: &label,
annotation_type: AnnotationType::Error,

View File

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

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::prelude::Constant;
use rustpython_parser::ast::Constant;
use crate::checkers::ast::Checker;

View File

@@ -9,6 +9,38 @@ use crate::registry::Rule;
use super::super::helpers::is_sys;
/// ## What it does
/// Checks for comparisons that test `sys.version` against string literals,
/// such that the comparison will evaluate to `False` on Python 3.10 or later.
///
/// ## Why is this bad?
/// Comparing `sys.version` to a string is error-prone and may cause subtle
/// bugs, as the comparison will be performed lexicographically, not
/// semantically. For example, `sys.version > "3.9"` will evaluate to `False`
/// when using Python 3.10, as `"3.10"` is lexicographically "less" than
/// `"3.9"`.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version > "3.9" # `False` on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info > (3, 9) # `True` on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr3;
@@ -19,6 +51,43 @@ impl Violation for SysVersionCmpStr3 {
}
}
/// ## What it does
/// Checks for equality comparisons against the major version returned by
/// `sys.version_info` (e.g., `sys.version_info[0] == 3`).
///
/// ## Why is this bad?
/// Using `sys.version_info[0] == 3` to verify that the major version is
/// Python 3 or greater will fail if the major version number is ever
/// incremented (e.g., to Python 4). This is likely unintended, as code
/// that uses this comparison is likely intended to be run on Python 2,
/// but would now run on Python 4 too.
///
/// Instead, use `>=` to check if the major version number is 3 or greater,
/// to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info[0] == 3:
/// ...
/// else:
/// print("Python 2") # This will be printed on Python 4.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info >= (3,):
/// ...
/// else:
/// print("Python 2") # This will not be printed on Python 4.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo0Eq3;
@@ -29,6 +98,36 @@ impl Violation for SysVersionInfo0Eq3 {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version_info[1]` against an integer.
///
/// ## Why is this bad?
/// Comparisons based on the current minor version number alone can cause
/// subtle bugs and would likely lead to unintended effects if the Python
/// major version number were ever incremented (e.g., to Python 4).
///
/// Instead, compare `sys.version_info` to a tuple, including the major and
/// minor version numbers, to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info[1] < 7:
/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info < (3, 7):
/// print("Python 3.6 or earlier.")
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo1CmpInt;
@@ -42,6 +141,36 @@ impl Violation for SysVersionInfo1CmpInt {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version_info.minor` against an integer.
///
/// ## Why is this bad?
/// Comparisons based on the current minor version number alone can cause
/// subtle bugs and would likely lead to unintended effects if the Python
/// major version number were ever incremented (e.g., to Python 4).
///
/// Instead, compare `sys.version_info` to a tuple, including the major and
/// minor version numbers, to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info.minor < 7:
/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info < (3, 7):
/// print("Python 3.6 or earlier.")
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfoMinorCmpInt;
@@ -55,6 +184,37 @@ impl Violation for SysVersionInfoMinorCmpInt {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version` against string literals,
/// such that the comparison would fail if the major version number were
/// ever incremented to Python 10 or higher.
///
/// ## Why is this bad?
/// Comparing `sys.version` to a string is error-prone and may cause subtle
/// bugs, as the comparison will be performed lexicographically, not
/// semantically.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version >= "3" # `False` on Python 10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info >= (3,) # `True` on Python 10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr10;

View File

@@ -5,6 +5,35 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `six.PY3`.
///
/// ## Why is this bad?
/// `six.PY3` will evaluate to `False` on Python 4 and greater. This is likely
/// unintended, and may cause code intended to run on Python 2 to run on Python 4
/// too.
///
/// Instead, use `not six.PY2` to validate that the current Python major version is
/// _not_ equal to 2, to future-proof the code.
///
/// ## Example
/// ```python
/// import six
///
/// six.PY3 # `False` on Python 4.
/// ```
///
/// Use instead:
/// ```python
/// import six
///
/// not six.PY2 # `True` on Python 4.
/// ```
///
/// ## References
/// - [PyPI: `six`](https://pypi.org/project/six/)
/// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2)
/// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3)
#[violation]
pub struct SixPY3;

View File

@@ -8,6 +8,36 @@ use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::flake8_2020::helpers::is_sys;
/// ## What it does
/// Checks for uses of `sys.version[:3]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[:3]` will truncate the version number (e.g., `"3.10"` would
/// become `"3.1"`). This is likely unintended, and can lead to subtle bugs if
/// the version string is used to test against a specific Python version.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[:3] # Evaluates to "3.1" on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info[:2] # Evaluates to (3, 10) on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice3;
@@ -18,6 +48,36 @@ impl Violation for SysVersionSlice3 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[2]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[2]` will select the first digit of the minor number only
/// (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, and
/// can lead to subtle bugs if the version is used to test against a minor
/// version number.
///
/// Instead, use `sys.version_info.minor` to access the current minor version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[2] # Evaluates to "1" on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.minor}" # Evaluates to "10" on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion2;
@@ -28,6 +88,36 @@ impl Violation for SysVersion2 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[0]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[0]` will select the first digit of the major version number
/// only (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended,
/// and can lead to subtle bugs if the version string is used to test against a
/// major version number.
///
/// Instead, use `sys.version_info.major` to access the current major version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[0] # If using Python 10, this evaluates to "1".
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10".
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion0;
@@ -38,6 +128,36 @@ impl Violation for SysVersion0 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[:1]`.
///
/// ## Why is this bad?
/// If the major version number consists of more than one digit, this will
/// select the first digit of the major version number only (e.g., `"10.0"`
/// would evaluate to `"1"`). This is likely unintended, and can lead to subtle
/// bugs in future versions of Python if the version string is used to test
/// against a specific major version number.
///
/// Instead, use `sys.version_info.major` to access the current major version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[:1] # If using Python 10, this evaluates to "1".
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10".
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice1;

View File

@@ -457,11 +457,7 @@ pub(crate) fn definition(
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member {
kind,
stmt,
..
}) = definition else {
let Definition::Member(Member { kind, stmt, .. }) = definition else {
return vec![];
};

View File

@@ -1,5 +1,6 @@
use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -36,9 +37,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
}
/// S107
pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) {
for ArgWithDefault {
def,
default,
@@ -53,9 +52,7 @@ pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnosti
continue;
};
if let Some(diagnostic) = check_password_kwarg(def, default) {
diagnostics.push(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
diagnostics
}

View File

@@ -1,5 +1,6 @@
use rustpython_parser::ast::{Keyword, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -22,10 +23,10 @@ impl Violation for HardcodedPasswordFuncArg {
}
/// S106
pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> {
keywords
.iter()
.filter_map(|keyword| {
pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) {
checker
.diagnostics
.extend(keywords.iter().filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) {
@@ -37,6 +38,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnosti
},
keyword.range(),
))
})
.collect()
}));
}

View File

@@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::{matches_password_name, string_literal};
#[violation]
@@ -47,12 +49,13 @@ fn password_target(target: &Expr) -> Option<&str> {
/// S105
pub(crate) fn compare_to_hardcoded_password_string(
checker: &mut Checker,
left: &Expr,
comparators: &[Expr],
) -> Vec<Diagnostic> {
comparators
.iter()
.filter_map(|comp| {
) {
checker
.diagnostics
.extend(comparators.iter().filter_map(|comp| {
string_literal(comp).filter(|string| !string.is_empty())?;
let Some(name) = password_target(left) else {
return None;
@@ -63,29 +66,29 @@ pub(crate) fn compare_to_hardcoded_password_string(
},
comp.range(),
))
})
.collect()
}));
}
/// S105
pub(crate) fn assign_hardcoded_password_string(
checker: &mut Checker,
value: &Expr,
targets: &[Expr],
) -> Option<Diagnostic> {
) {
if string_literal(value)
.filter(|string| !string.is_empty())
.is_some()
{
for target in targets {
if let Some(name) = password_target(target) {
return Some(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
HardcodedPasswordString {
name: name.to_string(),
},
value.range(),
));
return;
}
}
}
None
}

View File

@@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
return None;
};
// Only evaluate the full BinOp, not the nested components.
let Expr::BinOp(_ )= parent else {
let Expr::BinOp(_) = parent else {
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
}

View File

@@ -21,21 +21,6 @@ impl Violation for RequestWithNoCertValidation {
}
}
const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
const HTTPX_METHODS: [&str; 11] = [
"get",
"options",
"head",
"post",
"put",
"patch",
"delete",
"request",
"stream",
"Client",
"AsyncClient",
];
/// S501
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
@@ -46,16 +31,13 @@ pub(crate) fn request_with_no_cert_validation(
if let Some(target) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| {
if call_path.len() == 2 {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) {
return Some("requests");
}
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) {
return Some("httpx");
}
.and_then(|call_path| match call_path.as_slice() {
["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => {
Some("requests")
}
None
["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
| "stream" | "Client" | "AsyncClient"] => Some("httpx"),
_ => None,
})
{
let call_args = SimpleCallArgs::new(args, keywords);

View File

@@ -1,31 +1,28 @@
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs};
use crate::checkers::ast::Checker;
#[violation]
pub struct RequestWithoutTimeout {
pub timeout: Option<String>,
implicit: bool,
}
impl Violation for RequestWithoutTimeout {
#[derive_message_formats]
fn message(&self) -> String {
let RequestWithoutTimeout { timeout } = self;
match timeout {
Some(value) => {
format!("Probable use of requests call with timeout set to `{value}`")
}
None => format!("Probable use of requests call without timeout"),
let RequestWithoutTimeout { implicit } = self;
if *implicit {
format!("Probable use of requests call without timeout")
} else {
format!("Probable use of requests call with timeout set to `None`")
}
}
}
const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
/// S113
pub(crate) fn request_without_timeout(
checker: &mut Checker,
@@ -37,30 +34,26 @@ pub(crate) fn request_without_timeout(
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
HTTP_VERBS
.iter()
.any(|func_name| call_path.as_slice() == ["requests", func_name])
matches!(
call_path.as_slice(),
[
"requests",
"get" | "options" | "head" | "post" | "put" | "patch" | "delete"
]
)
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout_arg) = call_args.keyword_argument("timeout") {
if let Some(timeout) = match timeout_arg {
Expr::Constant(ast::ExprConstant {
value: value @ Constant::None,
..
}) => Some(checker.generator().constant(value)),
_ => None,
} {
if let Some(timeout) = call_args.keyword_argument("timeout") {
if is_const_none(timeout) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout {
timeout: Some(timeout),
},
timeout_arg.range(),
RequestWithoutTimeout { implicit: false },
timeout.range(),
));
}
} else {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { timeout: None },
RequestWithoutTimeout { implicit: true },
func.range(),
));
}

View File

@@ -4,50 +4,57 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[
"append",
"assertEqual",
"assertEquals",
"assertNotEqual",
"assertNotEquals",
"bool",
"bytes",
"count",
"failIfEqual",
"failUnlessEqual",
"float",
"fromkeys",
"get",
"getattr",
"getboolean",
"getfloat",
"getint",
"index",
"insert",
"int",
"param",
"pop",
"remove",
"set_blocking",
"set_enabled",
"setattr",
"__setattr__",
"setdefault",
"str",
];
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
matches!(
name,
"append"
| "assertEqual"
| "assertEquals"
| "assertNotEqual"
| "assertNotEquals"
| "bool"
| "bytes"
| "count"
| "failIfEqual"
| "failUnlessEqual"
| "float"
| "fromkeys"
| "get"
| "getattr"
| "getboolean"
| "getfloat"
| "getint"
| "index"
| "insert"
| "int"
| "param"
| "pop"
| "remove"
| "set_blocking"
| "set_enabled"
| "setattr"
| "__setattr__"
| "setdefault"
| "str"
)
}
pub(super) const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"];
/// Returns `true` if a function definition is allowed to use a boolean trap.
pub(super) fn is_allowed_func_def(name: &str) -> bool {
matches!(name, "__setitem__")
}
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref());
return is_allowed_func_call(attr);
}
if let Expr::Name(ast::ExprName { id, .. }) = func {
return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref());
return is_allowed_func_call(id);
}
false

View File

@@ -1,14 +1,11 @@
use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator};
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::add_if_boolean;
use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST;
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def};
/// ## What it does
/// Checks for the use of booleans as default values in function definitions.
@@ -64,7 +61,7 @@ pub(crate) fn check_boolean_default_value_in_function_definition(
decorator_list: &[Decorator],
arguments: &Arguments,
) {
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
if is_allowed_func_def(name) {
return;
}

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## What it does
/// Checks for boolean positional arguments in function definitions.
@@ -82,7 +82,7 @@ pub(crate) fn check_positional_boolean_in_def(
decorator_list: &[Decorator],
arguments: &Arguments,
) {
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
if is_allowed_func_def(name) {
return;
}

View File

@@ -161,19 +161,19 @@ pub(crate) fn abstract_base_class(
continue;
}
let (
Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})
) = stmt else {
let (Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
else {
continue;
};

View File

@@ -1,22 +1,20 @@
use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem};
use std::fmt;
use rustpython_parser::ast::{self, Expr, Ranged, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AssertionKind {
AssertRaises,
PytestRaises,
}
/// ## What it does
/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`.
/// Checks for `assertRaises` and `pytest.raises` context managers that catch
/// `Exception` or `BaseException`.
///
/// ## Why is this bad?
/// These forms catch every `Exception`, which can lead to tests passing even
/// if, e.g., the code being tested is never executed due to a typo.
/// if, e.g., the code under consideration raises a `SyntaxError` or
/// `IndentationError`.
///
/// Either assert for a more specific exception (builtin or custom), or use
/// `assertRaisesRegex` or `pytest.raises(..., match=<REGEX>)` respectively.
@@ -32,51 +30,83 @@ pub(crate) enum AssertionKind {
/// ```
#[violation]
pub struct AssertRaisesException {
kind: AssertionKind,
assertion: AssertionKind,
exception: ExceptionKind,
}
impl Violation for AssertRaisesException {
#[derive_message_formats]
fn message(&self) -> String {
match self.kind {
AssertionKind::AssertRaises => {
format!("`assertRaises(Exception)` should be considered evil")
}
AssertionKind::PytestRaises => {
format!("`pytest.raises(Exception)` should be considered evil")
}
let AssertRaisesException {
assertion,
exception,
} = self;
format!("`{assertion}({exception})` should be considered evil")
}
}
#[derive(Debug, PartialEq, Eq)]
enum AssertionKind {
AssertRaises,
PytestRaises,
}
impl fmt::Display for AssertionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
AssertionKind::AssertRaises => fmt.write_str("assertRaises"),
AssertionKind::PytestRaises => fmt.write_str("pytest.raises"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum ExceptionKind {
BaseException,
Exception,
}
impl fmt::Display for ExceptionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
ExceptionKind::BaseException => fmt.write_str("BaseException"),
ExceptionKind::Exception => fmt.write_str("Exception"),
}
}
}
/// B017
pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) {
let Some(item) = items.first() else {
return;
};
let item_context = &item.context_expr;
let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else {
return;
};
if args.len() != 1 {
return;
}
if item.optional_vars.is_some() {
return;
}
pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) {
for item in items {
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
range: _,
}) = &item.context_expr
else {
return;
};
if args.len() != 1 {
return;
}
if item.optional_vars.is_some() {
return;
}
if !checker
.semantic()
.resolve_call_path(args.first().unwrap())
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["", "Exception"])
})
{
return;
}
let Some(exception) = checker
.semantic()
.resolve_call_path(args.first().unwrap())
.and_then(|call_path| match call_path.as_slice() {
["", "Exception"] => Some(ExceptionKind::Exception),
["", "BaseException"] => Some(ExceptionKind::BaseException),
_ => None,
})
else {
return;
};
let kind = {
if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
{
AssertionKind::AssertRaises
} else if checker
@@ -92,11 +122,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
AssertionKind::PytestRaises
} else {
return;
}
};
};
checker.diagnostics.push(Diagnostic::new(
AssertRaisesException { kind },
stmt.range(),
));
checker.diagnostics.push(Diagnostic::new(
AssertRaisesException {
assertion,
exception,
},
item.range(),
));
}
}

View File

@@ -59,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr])
if attr != "environ" {
return;
}
let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id != "os" {

View File

@@ -166,7 +166,11 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand
let mut seen: FxHashSet<CallPath> = FxHashSet::default();
let mut duplicates: FxHashMap<CallPath, Vec<&Expr>> = FxHashMap::default();
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_: Some(type_),
..
}) = handler
else {
continue;
};
match type_.as_ref() {

View File

@@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses {
/// This should leave any unstarred iterables alone (subsequently raising a
/// warning for B029).
fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> {
let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else {
return vec![expr];
};
let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len());

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{self, Expr, Stmt};
use rustpython_parser::ast::{self, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -50,9 +50,9 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return;
};
let Expr::JoinedStr ( _) = value.as_ref() else {
if !value.is_joined_str_expr() {
return;
};
}
checker
.diagnostics
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));

View File

@@ -1,9 +1,8 @@
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::collect_arg_names;
use ruff_python_ast::helpers::includes_arg_name;
use ruff_python_ast::types::Node;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -58,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable {
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
// Tuple of: name, defining expression, and defining range.
loaded: Vec<(&'a str, &'a Expr)>,
// Tuple of: name, defining expression, and defining range.
stored: Vec<(&'a str, &'a Expr)>,
loaded: Vec<&'a ast::ExprName>,
stored: Vec<&'a ast::ExprName>,
}
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx {
ExprContext::Load => self.loaded.push((id, expr)),
ExprContext::Store => self.stored.push((id, expr)),
Expr::Name(name) => match &name.ctx {
ExprContext::Load => self.loaded.push(name),
ExprContext::Store => self.stored.push(name),
ExprContext::Del => {}
},
_ => visitor::walk_expr(self, expr),
@@ -80,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
#[derive(Default)]
struct SuspiciousVariablesVisitor<'a> {
names: Vec<(&'a str, &'a Expr)>,
names: Vec<&'a ast::ExprName>,
safe_functions: Vec<&'a Expr>,
}
@@ -95,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_body(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.loaded
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return;
}
Stmt::Return(ast::StmtReturn {
@@ -132,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
}) => {
match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => {
let id = id.as_str();
if id == "filter" || id == "reduce" || id == "map" {
if matches!(id.as_str(), "filter" | "reduce" | "map") {
for arg in args {
if matches!(arg, Expr::Lambda(_)) {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
}
@@ -159,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
for keyword in keywords {
if keyword.arg.as_ref().map_or(false, |arg| arg == "key")
&& matches!(keyword.value, Expr::Lambda(_))
&& keyword.value.is_lambda_expr()
{
self.safe_functions.push(&keyword.value);
}
@@ -175,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.loaded
.iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return;
}
@@ -198,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
#[derive(Default)]
struct NamesFromAssignmentsVisitor<'a> {
names: FxHashSet<&'a str>,
names: Vec<&'a str>,
}
/// `Visitor` to collect all names used in an assignment expression.
@@ -206,7 +207,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
self.names.insert(id.as_str());
self.names.push(id.as_str());
}
Expr::Starred(ast::ExprStarred { value, .. }) => {
self.visit_expr(value);
@@ -223,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
#[derive(Default)]
struct AssignedNamesVisitor<'a> {
names: FxHashSet<&'a str>,
names: Vec<&'a str>,
}
/// `Visitor` to collect all used identifiers in a statement.
@@ -257,7 +258,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
fn visit_expr(&mut self, expr: &'a Expr) {
if matches!(expr, Expr::Lambda(_)) {
if expr.is_lambda_expr() {
// Don't recurse.
return;
}
@@ -300,15 +301,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &
// If a variable was used in a function or lambda body, and assigned in the
// loop, flag it.
for (name, expr) in suspicious_variables {
if reassigned_in_loop.contains(name) {
if !checker.flake8_bugbear_seen.contains(&expr) {
checker.flake8_bugbear_seen.push(expr);
for name in suspicious_variables {
if reassigned_in_loop.contains(&name.id.as_str()) {
if !checker.flake8_bugbear_seen.contains(&name) {
checker.flake8_bugbear_seen.push(name);
checker.diagnostics.push(Diagnostic::new(
FunctionUsesLoopVariable {
name: name.to_string(),
name: name.id.to_string(),
},
expr.range(),
name.range(),
));
}
}

View File

@@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant(
func: &Expr,
args: &[Expr],
) {
let Expr::Name(ast::ExprName { id, .. } )= func else {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if id != "getattr" {
@@ -76,7 +76,8 @@ pub(crate) fn getattr_with_constant(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
} )= arg else {
}) = arg
else {
return;
};
if !is_identifier(value) {

View File

@@ -69,7 +69,7 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
{
let Some(default)= default else {
let Some(default) = default else {
continue;
};

View File

@@ -59,7 +59,11 @@ pub(crate) fn redundant_tuple_in_exception_handler(
handlers: &[ExceptHandler],
) {
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_: Some(type_),
..
}) = handler
else {
continue;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else {

View File

@@ -82,7 +82,8 @@ pub(crate) fn setattr_with_constant(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name),
..
} )= name else {
}) = name
else {
return;
};
if !is_identifier(name) {

View File

@@ -64,7 +64,7 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg(
return;
};
for arg in args {
let Expr::Starred (_) = arg else {
let Expr::Starred(_) = arg else {
continue;
};
if arg.start() <= keyword.start() {

View File

@@ -62,7 +62,8 @@ pub(crate) fn strip_with_multi_characters(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
} )= &args[0] else {
}) = &args[0]
else {
return;
};

View File

@@ -45,9 +45,9 @@ pub(crate) fn unary_prefix_increment(
if !matches!(op, UnaryOp::UAdd) {
return;
}
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else {
return;
};
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else {
return;
};
if !matches!(op, UnaryOp::UAdd) {
return;
}

View File

@@ -63,8 +63,8 @@ pub(crate) fn unreliable_callable_check(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
..
}) = &args[1] else
{
}) = &args[1]
else {
return;
};
if s != "__call__" {

View File

@@ -68,7 +68,13 @@ pub(crate) fn zip_without_explicit_strict(
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else {
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
..
}) = &arg
else {
return false;
};

View File

@@ -1,27 +1,38 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B017.py:23:9: B017 `assertRaises(Exception)` should be considered evil
B017.py:23:14: B017 `assertRaises(Exception)` should be considered evil
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| _________^
24 | | raise Exception("Evil I say!")
| |__________________________________________^ B017
25 |
26 | def context_manager_raises(self) -> None:
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
24 | raise Exception("Evil I say!")
|
B017.py:41:5: B017 `pytest.raises(Exception)` should be considered evil
B017.py:27:14: B017 `assertRaises(BaseException)` should be considered evil
|
40 | def test_pytest_raises():
41 | with pytest.raises(Exception):
| _____^
42 | | raise ValueError("Hello")
| |_________________________________^ B017
43 |
44 | with pytest.raises(Exception, "hello"):
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
28 | raise Exception("Evil I say!")
|
B017.py:45:10: B017 `pytest.raises(Exception)` should be considered evil
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
46 | raise ValueError("Hello")
|
B017.py:48:10: B017 `pytest.raises(Exception)` should be considered evil
|
46 | raise ValueError("Hello")
47 |
48 | with pytest.raises(Exception), pytest.raises(ValueError):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
49 | raise ValueError("Hello")
|

View File

@@ -109,7 +109,8 @@ pub(crate) fn fix_unnecessary_generator_dict(
// Extract the (k, v) from `(k, v) for ...`.
let generator_exp = match_generator_exp(&arg.value)?;
let tuple = match_tuple(&generator_exp.elt)?;
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] else {
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..]
else {
bail!("Expected tuple to contain two elements");
};
@@ -188,9 +189,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
let tuple = match_tuple(&list_comp.elt)?;
let [Element::Simple {
value: key, ..
}, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); };
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..]
else {
bail!("Expected tuple with two elements");
};
tree = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()),
@@ -982,14 +984,10 @@ pub(crate) fn fix_unnecessary_map(
}
let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
bail!(
"Expected tuple to contain a key as the first element"
);
bail!("Expected tuple to contain a key as the first element");
};
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
bail!(
"Expected tuple to contain a key as the second element"
);
bail!("Expected tuple to contain a key as the second element");
};
(key, value)
@@ -1063,9 +1061,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let call = match_call_mut(&mut tree)?;
let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!(
"Expected Expression::ListComp"
);
bail!("Expected Expression::ListComp");
};
let mut new_empty_lines = vec![];

View File

@@ -66,11 +66,13 @@ pub(crate) fn unnecessary_comprehension_any_all(
if !keywords.is_empty() {
return;
}
let Expr::Name(ast::ExprName { id, .. } )= func else {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 {
let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else {
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0]
else {
return;
};
if contains_await(elt) {

View File

@@ -84,7 +84,7 @@ 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, .. }) = arg else {
return;
};
let Some(inner) = helpers::expr_name(func) else {

View File

@@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument {

View File

@@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_list(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("list", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("list") {

View File

@@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("dict") {

View File

@@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@@ -54,7 +54,9 @@ pub(crate) fn unnecessary_literal_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("dict") {

View File

@@ -55,7 +55,9 @@ pub(crate) fn unnecessary_literal_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@@ -83,7 +83,7 @@ pub(crate) fn unnecessary_map(
)
}
let Some(id) = helpers::expr_name(func) else {
let Some(id) = helpers::expr_name(func) else {
return;
};
match id {
@@ -127,9 +127,11 @@ pub(crate) fn unnecessary_map(
if args.len() != 2 {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else {
return;
};
let Some(argument) =
helpers::first_argument_with_matching_function("map", func, args)
else {
return;
};
if let Expr::Lambda(_) = argument {
let mut diagnostic = create_diagnostic(id, expr.range());
if checker.patch(diagnostic.kind.rule()) {
@@ -155,7 +157,9 @@ pub(crate) fn unnecessary_map(
if args.len() == 1 {
if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] {
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("map", func, args)
else {
return;
};
if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument {

View File

@@ -64,9 +64,15 @@ pub(crate) fn unnecessary_subscript_reversal(
let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else {
return;
};
let Expr::Slice(ast::ExprSlice { lower, upper, step, range: _ }) = slice.as_ref() else {
return;
};
let Expr::Slice(ast::ExprSlice {
lower,
upper,
step,
range: _,
}) = slice.as_ref()
else {
return;
};
if lower.is_some() || upper.is_some() {
return;
}
@@ -77,13 +83,15 @@ pub(crate) fn unnecessary_subscript_reversal(
op: UnaryOp::USub,
operand,
range: _,
}) = step.as_ref() else {
}) = step.as_ref()
else {
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(val),
..
}) = operand.as_ref() else {
}) = operand.as_ref()
else {
return;
};
if *val != BigInt::from(1) {

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