Compare commits

..

44 Commits

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

Tests: No snapshots changed

---------

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

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

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

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

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

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

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

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

## Test Plan

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

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

## Test Plan

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

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

## Test Plan

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

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

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

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

## Test Plan

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

Fixes #5794

## Test Plan

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

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

Related to #2646.

## Test Plan

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

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

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

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

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

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

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

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

<details>

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

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

Overlong lines in CPython

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

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

</details>

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

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

## Test Plan

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

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

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

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

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

I updated all ruff crate descriptions in the contributing guide

## Test Plan

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

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

Fixes #3545

## Test Plan

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

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

Before:

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

After:

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

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

## Test Plan

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

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

## Test Plan

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

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

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

We already model these in the semantic model, so this PR just expands
the scope of the rule to handle those.
2023-07-15 15:37:34 -04:00
Charlie Marsh
2de6f30929 Lift Expr::Subscript value visit out of branches (#5783)
Like #5772, but for subscripts.
2023-07-15 15:12:15 -04:00
Micha Reiser
df2efe81c8 Respect magic trailing comma for set expression (#5782)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

## Test Plan
See the fixed black tests

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

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

## Test Plan

Added ruff fixture based on `ListComp`'s.
2023-07-15 17:35:23 +01:00
Micha Reiser
3cda89ecaf Parenthesize with statements (#5758)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

### Case 1:

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

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

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


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

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

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

Or 

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

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

```
## Test Plan

I added new snapshot tests

Improves the django similarity index from 0.973 to 0.977
2023-07-15 16:03:09 +01:00
Luc Khai Hai
e1c119fde3 Format SetComp (#5774)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

Format `SetComp` like `ListComp`.

## Test Plan

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

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

## Test Plan

A new test case

---------

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

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

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

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

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

No behavior changes expected.

## Test Plan

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

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

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

Closes #5692.
2023-07-14 15:42:47 -04:00
162 changed files with 4421 additions and 2858 deletions

2
.gitignore vendored
View File

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

View File

@@ -110,27 +110,35 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
If you're working on a rule, this is the crate for you.
- `crates/ruff_benchmark`: binary crate for running micro-benchmarks.
- `crates/ruff_cache`: library crate for caching lint results.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`).
- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs.
- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate
representation.
`cargo dev generate-all`), see the [`cargo dev`](#cargo-dev) section below.
- `crates/ruff_diagnostics`: library crate for the rule-independent abstractions in the lint
diagnostics APIs.
- `crates/ruff_formatter`: library crate for language agnostic code formatting logic based on an
intermediate representation. The backend for `ruff_python_formatter`.
- `crates/ruff_index`: library crate inspired by `rustc_index`.
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic.
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities. Note
that the AST schema itself is defined in the
[rustpython-ast](https://github.com/astral-sh/RustPython-Parser) crate.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.
- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic,
including Ruff's semantic model.
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data.
including Ruff's semantic model. Used to resolve queries like "What import does this variable
refer to?"
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data, e.g.
the names of all built-in exceptions and which standard library types are immutable.
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
logic.
logic (indentation and newlines).
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff.
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the
[Ruff Playground](https://play.ruff.rs/).
### Example: Adding a new lint rule
@@ -411,6 +419,13 @@ Summary
159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython'
```
To benchmark a subset of rules, e.g. `LineTooLong` and `DocLineTooLong`:
```shell
cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e --select W505,E501"
```
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.

277
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import module
from module import Class
def f(var: Class) -> Class:
x: Class
def f(var: module.Class) -> module.Class:
x: module.Class
def f():
print(Class)
def f():
print(module.Class)

View File

@@ -1,6 +0,0 @@
from typing import TYPE_CHECKING
Class = ...
if TYPE_CHECKING:
from module import Class

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -115,7 +115,6 @@ pub(crate) fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
} else {
}
}
}

View File

@@ -604,6 +604,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(PandasVet, "012") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotReadTable),
(PandasVet, "013") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfDotStack),
(PandasVet, "015") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasUseOfPdMerge),
(PandasVet, "101") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck),
(PandasVet, "901") => (RuleGroup::Unspecified, rules::pandas_vet::rules::PandasDfVariableName),
// flake8-errmsg
@@ -707,7 +708,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "003") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
(Flake8TypeChecking, "004") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "200") => (RuleGroup::Unspecified, rules::flake8_type_checking::rules::UnquotedAnnotation),
// tryceratops
(Tryceratops, "002") => (RuleGroup::Unspecified, rules::tryceratops::rules::RaiseVanillaClass),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -208,12 +208,7 @@ fn check_positional_args(
(ErrorKind::ThirdArgBadAnnotation, is_traceback_type),
];
for (arg, (error_info, predicate)) in positional_args
.iter()
.skip(1)
.take(3)
.zip(validations.into_iter())
{
for (arg, (error_info, predicate)) in positional_args.iter().skip(1).take(3).zip(validations) {
let Some(annotation) = arg.def.annotation.as_ref() else {
continue;
};

View File

@@ -11,7 +11,7 @@ use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged,
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_comments_in, Truthiness};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{visitor, whitespace};
@@ -197,7 +197,7 @@ pub(crate) fn unittest_assertion(
if checker.semantic().stmt().is_expr_stmt()
&& checker.semantic().expr_parent().is_none()
&& !checker.semantic().scope().kind.is_lambda()
&& !has_comments_in(expr.range(), checker.locator)
&& !checker.indexer.comment_ranges().intersects(expr.range())
{
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
@@ -483,7 +483,7 @@ pub(crate) fn composite_condition(
if checker.patch(diagnostic.kind.rule()) {
if matches!(composite, CompositionKind::Simple)
&& msg.is_none()
&& !has_comments_in(stmt.range(), checker.locator)
&& !checker.indexer.comment_ranges().intersects(stmt.range())
{
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {

View File

@@ -505,7 +505,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
}
// Avoid removing comments.
if has_comments(expr, checker.locator) {
if has_comments(expr, checker.locator, checker.indexer) {
continue;
}

View File

@@ -6,9 +6,7 @@ use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifie
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
use ruff_python_ast::helpers::{
any_over_expr, contains_effect, first_colon_range, has_comments, has_comments_in,
};
use ruff_python_ast::helpers::{any_over_expr, contains_effect, first_colon_range, has_comments};
use ruff_python_semantic::SemanticModel;
use ruff_python_whitespace::UniversalNewlines;
@@ -378,10 +376,11 @@ pub(crate) fn nested_if_statements(
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
let nested_if = &body[0];
if !has_comments_in(
TextRange::new(stmt.start(), nested_if.start()),
checker.locator,
) {
if !checker
.indexer
.comment_ranges()
.intersects(TextRange::new(stmt.start(), nested_if.start()))
{
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
Ok(edit) => {
if edit
@@ -464,7 +463,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
if checker.patch(diagnostic.kind.rule()) {
if matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !has_comments(stmt, checker.locator)
&& !has_comments(stmt, checker.locator, checker.indexer)
&& (test.is_compare_expr() || checker.semantic().is_builtin("bool"))
{
if test.is_compare_expr() {
@@ -650,7 +649,7 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator) {
if !has_comments(stmt, checker.locator, checker.indexer) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
stmt.range(),
@@ -1050,7 +1049,7 @@ pub(crate) fn use_dict_get_with_default(
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator) {
if !has_comments(stmt, checker.locator, checker.indexer) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
stmt.range(),

View File

@@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Ranged, Stmt, WithItem};
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{first_colon_range, has_comments_in};
use ruff_python_ast::helpers::first_colon_range;
use ruff_python_whitespace::UniversalNewlines;
use crate::checkers::ast::Checker;
@@ -129,10 +129,11 @@ pub(crate) fn multiple_with_statements(
),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments_in(
TextRange::new(with_stmt.start(), with_body[0].start()),
checker.locator,
) {
if !checker
.indexer
.comment_ranges()
.intersects(TextRange::new(with_stmt.start(), with_body[0].start()))
{
match fix_with::fix_multiple_with_statements(
checker.locator,
checker.stylist,

View File

@@ -117,7 +117,7 @@ pub(crate) fn suppressible_exception(
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator) {
if !has_comments(stmt, checker.locator, checker.indexer) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer.get_or_import_symbol(
&ImportRequest::import("contextlib", "suppress"),

View File

@@ -15,13 +15,10 @@ mod tests {
use crate::test::{test_path, test_snippet};
use crate::{assert_messages, settings};
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_1.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_2.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_3.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_4.py"))]
@@ -30,12 +27,13 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
#[test_case(Rule::UnquotedAnnotation, Path::new("TCH200_0.py"))]
#[test_case(Rule::UnquotedAnnotation, Path::new("TCH200_1.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,9 +1,7 @@
pub(crate) use empty_type_checking_block::*;
pub(crate) use quoted_annotation::*;
pub(crate) use runtime_import_in_type_checking_block::*;
pub(crate) use typing_only_runtime_import::*;
mod empty_type_checking_block;
mod quoted_annotation;
mod runtime_import_in_type_checking_block;
mod typing_only_runtime_import;

View File

@@ -1,100 +0,0 @@
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::BindingId;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for the presence of unnecessary quotes in type annotations.
///
/// ## Why is this bad?
/// In Python, type annotations can be quoted to avoid forward references.
/// However, if `from __future__ import annotations` is present, Python
/// will always evaluate type annotations in a deferred manner, making
/// the quotes unnecessary.
///
/// ## Example
/// ```python
/// from __future__ import annotations
///
///
/// def foo(bar: "Bar") -> "Bar":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from __future__ import annotations
///
///
/// def foo(bar: Bar) -> Bar:
/// ...
/// ```
///
/// ## References
/// - [PEP 563](https://peps.python.org/pep-0563/)
/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__)
#[violation]
pub struct UnquotedAnnotation {
name: String,
}
impl Violation for UnquotedAnnotation {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let UnquotedAnnotation { name } = self;
format!("Typing-only variable referenced in runtime annotation: `{name}`")
}
fn autofix_title(&self) -> Option<String> {
Some("Add quotes".to_string())
}
}
/// TCH200
pub(crate) fn unquoted_annotation(checker: &mut Checker, binding_id: BindingId, expr: &Expr) {
// If we're already in a quoted annotation, skip.
if checker.semantic().in_deferred_type_definition() {
return;
}
// If we're in a typing-only context, skip.
if checker.semantic().execution_context().is_typing() {
return;
}
// If the reference resolved to a typing-only import, flag.
if checker.semantic().bindings[binding_id].context.is_typing() {
// Expand any attribute chains (e.g., flag `typing.List` in `typing.List[int]`).
let mut expr = expr;
for parent in checker.semantic().expr_ancestors() {
if parent.is_attribute_expr() {
expr = parent;
} else {
break;
}
}
let mut diagnostic = Diagnostic::new(
UnquotedAnnotation {
name: checker.locator.slice(expr.range()).to_string(),
},
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// We can only _fix_ this if we're in a type annotation.
if checker.semantic().in_runtime_annotation() {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("\"{}\"", checker.locator.slice(expr.range()).to_string()),
expr.range(),
)));
}
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,92 +0,0 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
---
TCH200_0.py:8:12: TCH200 [*] Typing-only variable referenced in runtime annotation: `Class`
|
8 | def f(var: Class) -> Class:
| ^^^^^ TCH200
9 | x: Class
|
= help: Add quotes
Fix
5 5 | from module import Class
6 6 |
7 7 |
8 |-def f(var: Class) -> Class:
8 |+def f(var: "Class") -> Class:
9 9 | x: Class
10 10 |
11 11 |
TCH200_0.py:8:22: TCH200 [*] Typing-only variable referenced in runtime annotation: `Class`
|
8 | def f(var: Class) -> Class:
| ^^^^^ TCH200
9 | x: Class
|
= help: Add quotes
Fix
5 5 | from module import Class
6 6 |
7 7 |
8 |-def f(var: Class) -> Class:
8 |+def f(var: Class) -> "Class":
9 9 | x: Class
10 10 |
11 11 |
TCH200_0.py:12:12: TCH200 [*] Typing-only variable referenced in runtime annotation: `module.Class`
|
12 | def f(var: module.Class) -> module.Class:
| ^^^^^^^^^^^^ TCH200
13 | x: module.Class
|
= help: Add quotes
Fix
9 9 | x: Class
10 10 |
11 11 |
12 |-def f(var: module.Class) -> module.Class:
12 |+def f(var: "module.Class") -> module.Class:
13 13 | x: module.Class
14 14 |
15 15 |
TCH200_0.py:12:29: TCH200 [*] Typing-only variable referenced in runtime annotation: `module.Class`
|
12 | def f(var: module.Class) -> module.Class:
| ^^^^^^^^^^^^ TCH200
13 | x: module.Class
|
= help: Add quotes
Fix
9 9 | x: Class
10 10 |
11 11 |
12 |-def f(var: module.Class) -> module.Class:
12 |+def f(var: module.Class) -> "module.Class":
13 13 | x: module.Class
14 14 |
15 15 |
TCH200_0.py:17:11: TCH200 Typing-only variable referenced in runtime annotation: `Class`
|
16 | def f():
17 | print(Class)
| ^^^^^ TCH200
|
= help: Add quotes
TCH200_0.py:21:11: TCH200 Typing-only variable referenced in runtime annotation: `module.Class`
|
20 | def f():
21 | print(module.Class)
| ^^^^^^^^^^^^ TCH200
|
= help: Add quotes

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
---

View File

@@ -777,6 +777,35 @@ mod tests {
Ok(())
}
#[test_case(Path::new("comment.py"))]
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]
#[test_case(Path::new("empty.py"))]
#[test_case(Path::new("existing_import.py"))]
#[test_case(Path::new("multiline_docstring.py"))]
#[test_case(Path::new("off.py"))]
fn required_import_with_alias(path: &Path) -> Result<()> {
let snapshot = format!("required_import_with_alias_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort/required_imports").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
required_imports: BTreeSet::from([
"from __future__ import annotations as _annotations".to_string(),
]),
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::MissingRequiredImport)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_only.py"))]

View File

@@ -0,0 +1,19 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
comment.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | #!/usr/bin/env python3
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | #!/usr/bin/env python3
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1

View File

@@ -0,0 +1,19 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | """Hello, world!"""
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -0,0 +1,17 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring_with_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""; x = \
| I002
2 | 1; y = 2
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |-"""Hello, world!"""; x = \
1 |+"""Hello, world!"""; from __future__ import annotations as _annotations; x = \
2 2 | 1; y = 2

View File

@@ -0,0 +1,15 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
docstring_with_semicolon.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """Hello, world!"""; x = 1
| I002
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |-"""Hello, world!"""; x = 1
1 |+"""Hello, world!"""; from __future__ import annotations as _annotations; x = 1

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---

View File

@@ -0,0 +1,17 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
existing_import.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | from __future__ import generator_stop
| I002
2 | import os
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 |+from __future__ import annotations as _annotations
1 2 | from __future__ import generator_stop
2 3 | import os

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
multiline_docstring.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | """a
| I002
2 | b"""
3 | # b
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | """a
2 2 | b"""
3 3 | # b
4 |+from __future__ import annotations as _annotations
4 5 | import os

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
off.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
1 | # isort: off
| I002
2 |
3 | x = 1
|
= help: Insert required import: `from future import annotations as _annotations`
Fix
1 1 | # isort: off
2 |+from __future__ import annotations as _annotations
2 3 |
3 4 | x = 1
4 5 | # isort: on

View File

@@ -237,27 +237,6 @@ mod tests {
"#,
"PD011_pass_node_name"
)]
#[test_case(
r#"
import pandas as pd
employees = pd.read_csv(input_file)
"#,
"PD012_pass_read_csv"
)]
#[test_case(
r#"
import pandas as pd
employees = pd.read_table(input_file)
"#,
"PD012_fail_read_table"
)]
#[test_case(
r#"
import pandas as pd
employees = read_table
"#,
"PD012_node_Name_pass"
)]
#[test_case(
r#"
import pandas as pd
@@ -360,7 +339,12 @@ mod tests {
assert_messages!(snapshot, diagnostics);
}
#[test_case(
Rule::PandasUseOfDotReadTable,
Path::new("pandas_use_of_dot_read_table.py")
)]
#[test_case(Rule::PandasUseOfInplaceArgument, Path::new("PD002.py"))]
#[test_case(Rule::PandasNuniqueConstantSeriesCheck, Path::new("PD101.py"))]
fn paths(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -126,18 +126,6 @@ impl Violation for PandasUseOfDotPivotOrUnstack {
}
}
// TODO(tjkuson): Add documentation for this rule once clarified.
// https://github.com/astral-sh/ruff/issues/5628
#[violation]
pub struct PandasUseOfDotReadTable;
impl Violation for PandasUseOfDotReadTable {
#[derive_message_formats]
fn message(&self) -> String {
format!("`.read_csv` is preferred to `.read_table`; provides same functionality")
}
}
/// ## What it does
/// Checks for uses of `.stack` on Pandas objects.
///
@@ -193,14 +181,6 @@ pub(crate) fn call(checker: &mut Checker, func: &Expr) {
{
PandasUseOfDotPivotOrUnstack.into()
}
"read_table"
if checker
.settings
.rules
.enabled(Rule::PandasUseOfDotReadTable) =>
{
PandasUseOfDotReadTable.into()
}
"stack" if checker.settings.rules.enabled(Rule::PandasUseOfDotStack) => {
PandasUseOfDotStack.into()
}

View File

@@ -2,12 +2,16 @@ pub(crate) use assignment_to_df::*;
pub(crate) use attr::*;
pub(crate) use call::*;
pub(crate) use inplace_argument::*;
pub(crate) use nunique_constant_series_check::*;
pub(crate) use pd_merge::*;
pub(crate) use read_table::*;
pub(crate) use subscript::*;
pub(crate) mod assignment_to_df;
pub(crate) mod attr;
pub(crate) mod call;
pub(crate) mod inplace_argument;
pub(crate) mod nunique_constant_series_check;
pub(crate) mod pd_merge;
pub(crate) mod read_table;
pub(crate) mod subscript;

View File

@@ -0,0 +1,122 @@
use num_traits::One;
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::rules::pandas_vet::helpers::{test_expression, Resolution};
/// ## What it does
/// Check for uses of `.nunique()` to check if a Pandas Series is constant
/// (i.e., contains only one unique value).
///
/// ## Why is this bad?
/// `.nunique()` is computationally inefficient for checking if a Series is
/// constant.
///
/// Consider, for example, a Series of length `n` that consists of increasing
/// integer values (e.g., 1, 2, 3, 4). The `.nunique()` method will iterate
/// over the entire Series to count the number of unique values. But in this
/// case, we can detect that the Series is non-constant after visiting the
/// first two values, which are non-equal.
///
/// In general, `.nunique()` requires iterating over the entire Series, while a
/// more efficient approach allows short-circuiting the operation as soon as a
/// non-equal value is found.
///
/// Instead of calling `.nunique()`, convert the Series to a NumPy array, and
/// check if all values in the array are equal to the first observed value.
///
/// ## Example
/// ```python
/// import pandas as pd
///
/// data = pd.Series(range(1000))
/// if data.nunique() <= 1:
/// print("Series is constant")
/// ```
///
/// Use instead:
/// ```python
/// import pandas as pd
///
/// data = pd.Series(range(1000))
/// array = data.to_numpy()
/// if array.shape[0] == 0 or (array[0] == array).all():
/// print("Series is constant")
/// ```
///
/// ## References
/// - [Pandas Cookbook: "Constant Series"](https://pandas.pydata.org/docs/user_guide/cookbook.html#constant-series)
/// - [Pandas documentation: `nunique`](https://pandas.pydata.org/docs/reference/api/pandas.Series.nunique.html)
#[violation]
pub struct PandasNuniqueConstantSeriesCheck;
impl Violation for PandasNuniqueConstantSeriesCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `series.nunique()` for checking that a series is constant is inefficient")
}
}
/// PD101
pub(crate) fn nunique_constant_series_check(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[CmpOp],
comparators: &[Expr],
) {
let ([op], [right]) = (ops, comparators) else {
return;
};
// Operators may be ==, !=, <=, >.
if !matches!(op, CmpOp::Eq | CmpOp::NotEq | CmpOp::LtE | CmpOp::Gt,) {
return;
}
// Right should be the integer 1.
if !is_constant_one(right) {
return;
}
// Check if call is `.nuniuqe()`.
let Expr::Call(ast::ExprCall { func, .. }) = left else {
return;
};
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
return;
};
if attr.as_str() != "nunique" {
return;
}
// Avoid flagging on non-Series (e.g., `{"a": 1}.at[0]`).
if !matches!(
test_expression(value, checker.semantic()),
Resolution::RelevantLocal
) {
return;
}
checker.diagnostics.push(Diagnostic::new(
PandasNuniqueConstantSeriesCheck,
expr.range(),
));
}
/// Return `true` if an [`Expr`] is a constant `1`.
fn is_constant_one(expr: &Expr) -> bool {
match expr {
Expr::Constant(constant) => match &constant.value {
Constant::Int(int) => int.is_one(),
_ => false,
},
_ => false,
}
}

View File

@@ -0,0 +1,76 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `pd.read_table` to read CSV files.
///
/// ## Why is this bad?
/// In the Pandas API, `pd.read_csv` and `pd.read_table` are equivalent apart
/// from their default separator: `pd.read_csv` defaults to a comma (`,`),
/// while `pd.read_table` defaults to a tab (`\t`) as the default separator.
///
/// Prefer `pd.read_csv` over `pd.read_table` when reading comma-separated
/// data (like CSV files), as it is more idiomatic.
///
/// ## Example
/// ```python
/// import pandas as pd
///
/// cities_df = pd.read_table("cities.csv", sep=",")
/// ```
///
/// Use instead:
/// ```python
/// import pandas as pd
///
/// cities_df = pd.read_csv("cities.csv")
/// ```
///
/// ## References
/// - [Pandas documentation: `read_csv`](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html#pandas.read_csv)
/// - [Pandas documentation: `read_table`](https://pandas.pydata.org/docs/reference/api/pandas.read_table.html#pandas.read_table)
#[violation]
pub struct PandasUseOfDotReadTable;
impl Violation for PandasUseOfDotReadTable {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `.read_csv` instead of `.read_table` to read CSV files")
}
}
/// PD012
pub(crate) fn use_of_read_table(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
if checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["pandas", "read_table"])
})
{
if let Some(Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
})) = keywords
.iter()
.find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |keyword| keyword.as_str() == "sep")
})
.map(|keyword| &keyword.value)
{
if value.as_str() == "," {
checker
.diagnostics
.push(Diagnostic::new(PandasUseOfDotReadTable, func.range()));
}
}
}
}

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
<filename>:3:13: PD012 `.read_csv` is preferred to `.read_table`; provides same functionality
|
2 | import pandas as pd
3 | employees = pd.read_table(input_file)
| ^^^^^^^^^^^^^ PD012
|

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---

View File

@@ -0,0 +1,42 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
pandas_use_of_dot_read_table.py:4:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
3 | # Errors.
4 | df = pd.read_table("data.csv", sep=",")
| ^^^^^^^^^^^^^ PD012
5 | df = pd.read_table("data.csv", sep=",", header=0)
6 | filename = "data.csv"
|
pandas_use_of_dot_read_table.py:5:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
3 | # Errors.
4 | df = pd.read_table("data.csv", sep=",")
5 | df = pd.read_table("data.csv", sep=",", header=0)
| ^^^^^^^^^^^^^ PD012
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
|
pandas_use_of_dot_read_table.py:7:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
5 | df = pd.read_table("data.csv", sep=",", header=0)
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
| ^^^^^^^^^^^^^ PD012
8 | df = pd.read_table(filename, sep=",", header=0)
|
pandas_use_of_dot_read_table.py:8:6: PD012 Use `.read_csv` instead of `.read_table` to read CSV files
|
6 | filename = "data.csv"
7 | df = pd.read_table(filename, sep=",")
8 | df = pd.read_table(filename, sep=",", header=0)
| ^^^^^^^^^^^^^ PD012
9 |
10 | # Non-errors.
|

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---

View File

@@ -0,0 +1,122 @@
---
source: crates/ruff/src/rules/pandas_vet/mod.rs
---
PD101.py:7:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
6 | # PD101
7 | data.nunique() <= 1
| ^^^^^^^^^^^^^^^^^^^ PD101
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
|
PD101.py:8:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
6 | # PD101
7 | data.nunique() <= 1
8 | data.nunique(dropna=True) <= 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
|
PD101.py:9:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
7 | data.nunique() <= 1
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
|
PD101.py:10:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
8 | data.nunique(dropna=True) <= 1
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
| ^^^^^^^^^^^^^^^^^^^ PD101
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
|
PD101.py:11:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
9 | data.nunique(dropna=False) <= 1
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
|
PD101.py:12:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
10 | data.nunique() == 1
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
|
PD101.py:13:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
11 | data.nunique(dropna=True) == 1
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
| ^^^^^^^^^^^^^^^^^^^ PD101
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
|
PD101.py:14:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
12 | data.nunique(dropna=False) == 1
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
|
PD101.py:15:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
13 | data.nunique() != 1
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
|
PD101.py:16:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
14 | data.nunique(dropna=True) != 1
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
| ^^^^^^^^^^^^^^^^^^ PD101
17 | data.dropna().nunique() == 1
18 | data[data.notnull()].nunique() == 1
|
PD101.py:17:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
15 | data.nunique(dropna=False) != 1
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
18 | data[data.notnull()].nunique() == 1
|
PD101.py:18:1: PD101 Using `series.nunique()` for checking that a series is constant is inefficient
|
16 | data.nunique() > 1
17 | data.dropna().nunique() == 1
18 | data[data.notnull()].nunique() == 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PD101
19 |
20 | # No violation of this rule
|

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -53,7 +53,6 @@ impl Violation for MixedCaseVariableInClassScope {
pub(crate) fn mixed_case_variable_in_class_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
bases: &[Expr],
) {
@@ -66,15 +65,22 @@ pub(crate) fn mixed_case_variable_in_class_scope(
{
return;
}
if helpers::is_mixed_case(name)
&& !helpers::is_named_tuple_assignment(stmt, checker.semantic())
&& !helpers::is_typed_dict_class(bases, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInClassScope {
name: name.to_string(),
},
expr.range(),
));
if !helpers::is_mixed_case(name) {
return;
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_class(bases, checker.semantic())
{
return;
}
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInClassScope {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -60,12 +60,7 @@ impl Violation for MixedCaseVariableInGlobalScope {
}
/// N816
pub(crate) fn mixed_case_variable_in_global_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
pub(crate) fn mixed_case_variable_in_global_scope(checker: &mut Checker, expr: &Expr, name: &str) {
if checker
.settings
.pep8_naming
@@ -75,13 +70,20 @@ pub(crate) fn mixed_case_variable_in_global_scope(
{
return;
}
if helpers::is_mixed_case(name) && !helpers::is_named_tuple_assignment(stmt, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInGlobalScope {
name: name.to_string(),
},
expr.range(),
));
if !helpers::is_mixed_case(name) {
return;
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic()) {
return;
}
checker.diagnostics.push(Diagnostic::new(
MixedCaseVariableInGlobalScope {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -50,12 +50,7 @@ impl Violation for NonLowercaseVariableInFunction {
}
/// N806
pub(crate) fn non_lowercase_variable_in_function(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &Expr, name: &str) {
if checker
.settings
.pep8_naming
@@ -66,16 +61,22 @@ pub(crate) fn non_lowercase_variable_in_function(
return;
}
if !str::is_lowercase(name)
&& !helpers::is_named_tuple_assignment(stmt, checker.semantic())
&& !helpers::is_typed_dict_assignment(stmt, checker.semantic())
&& !helpers::is_type_var_assignment(stmt, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
NonLowercaseVariableInFunction {
name: name.to_string(),
},
expr.range(),
));
if str::is_lowercase(name) {
return;
}
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_assignment(parent, checker.semantic())
|| helpers::is_type_var_assignment(parent, checker.semantic())
{
return;
}
checker.diagnostics.push(Diagnostic::new(
NonLowercaseVariableInFunction {
name: name.to_string(),
},
expr.range(),
));
}

View File

@@ -1,12 +1,12 @@
use std::fmt;
use regex::Regex;
use rustpython_parser::ast;
use rustpython_parser::ast::Expr;
use rustpython_parser::ast::Ranged;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -28,16 +28,16 @@ use crate::registry::AsRule;
///
/// ## Example
/// ```python
/// some_dict = {"a": 1, "b": 2}
/// for _, val in some_dict.items():
/// print(val)
/// obj = {"a": 1, "b": 2}
/// for key, value in obj.items():
/// print(value)
/// ```
///
/// Use instead:
/// ```python
/// some_dict = {"a": 1, "b": 2}
/// for val in some_dict.values():
/// print(val)
/// obj = {"a": 1, "b": 2}
/// for value in obj.values():
/// print(value)
/// ```
#[violation]
pub struct IncorrectDictIterator {
@@ -79,8 +79,8 @@ pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter
}
match (
is_ignored_tuple_or_name(key, &checker.settings.dummy_variable_rgx),
is_ignored_tuple_or_name(value, &checker.settings.dummy_variable_rgx),
is_unused(key, checker.semantic()),
is_unused(value, checker.semantic()),
) {
(true, true) => {
// Both the key and the value are unused.
@@ -142,13 +142,33 @@ impl fmt::Display for DictSubset {
}
}
/// Returns `true` if the given expression is either an ignored value or a tuple of ignored values.
fn is_ignored_tuple_or_name(expr: &Expr, dummy_variable_rgx: &Regex) -> bool {
/// Returns `true` if the given expression is either an unused value or a tuple of unused values.
fn is_unused(expr: &Expr, model: &SemanticModel) -> bool {
match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.all(|expr| is_ignored_tuple_or_name(expr, dummy_variable_rgx)),
Expr::Name(ast::ExprName { id, .. }) => dummy_variable_rgx.is_match(id.as_str()),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(|expr| is_unused(expr, model)),
Expr::Name(ast::ExprName { id, .. }) => {
// Treat a variable as used if it has any usages, _or_ it's shadowed by another variable
// with usages.
//
// If we don't respect shadowing, we'll incorrectly flag `bar` as unused in:
// ```python
// from random import random
//
// for bar in range(10):
// if random() > 0.5:
// break
// else:
// bar = 1
//
// print(bar)
// ```
let scope = model.scope();
scope
.get_all(id)
.map(|binding_id| model.binding(binding_id))
.filter(|binding| binding.range.start() >= expr.range().start())
.all(|binding| !binding.is_used())
}
_ => false,
}
}

View File

@@ -1,167 +1,211 @@
---
source: crates/ruff/src/rules/perflint/mod.rs
---
PERF102.py:3:17: PERF102 [*] When using only the values of a dict use the `values()` method
PERF102.py:5:21: PERF102 [*] When using only the values of a dict use the `values()` method
|
1 | some_dict = {"a": 12, "b": 32, "c": 44}
2 |
3 | for _, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
4 | print(value)
4 | def f():
5 | for _, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
6 | print(value)
|
= help: Replace `.items()` with `.values()`
Suggested fix
1 1 | some_dict = {"a": 12, "b": 32, "c": 44}
2 2 |
3 |-for _, value in some_dict.items(): # PERF102
3 |+for value in some_dict.values(): # PERF102
4 4 | print(value)
5 5 |
6 6 |
3 3 |
4 4 | def f():
5 |- for _, value in some_dict.items(): # PERF102
5 |+ for value in some_dict.values(): # PERF102
6 6 | print(value)
7 7 |
8 8 |
PERF102.py:7:15: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
7 | for key, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
8 | print(key)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
4 4 | print(value)
5 5 |
6 6 |
7 |-for key, _ in some_dict.items(): # PERF102
7 |+for key in some_dict.keys(): # PERF102
8 8 | print(key)
9 9 |
10 10 |
PERF102.py:11:26: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:10:19: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
11 | for weird_arg_name, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
12 | print(weird_arg_name)
9 | def f():
10 | for key, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
11 | print(key)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
8 8 | print(key)
9 9 |
10 10 |
11 |-for weird_arg_name, _ in some_dict.items(): # PERF102
11 |+for weird_arg_name in some_dict.keys(): # PERF102
12 12 | print(weird_arg_name)
7 7 |
8 8 |
9 9 | def f():
10 |- for key, _ in some_dict.items(): # PERF102
10 |+ for key in some_dict.keys(): # PERF102
11 11 | print(key)
12 12 |
13 13 |
14 14 |
PERF102.py:15:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:15:30: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
15 | for name, (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
16 | pass
14 | def f():
15 | for weird_arg_name, _ in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
16 | print(weird_arg_name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
12 12 | print(weird_arg_name)
12 12 |
13 13 |
14 14 |
15 |-for name, (_, _) in some_dict.items(): # PERF102
15 |+for name in some_dict.keys(): # PERF102
16 16 | pass
14 14 | def f():
15 |- for weird_arg_name, _ in some_dict.items(): # PERF102
15 |+ for weird_arg_name in some_dict.keys(): # PERF102
16 16 | print(weird_arg_name)
17 17 |
18 18 |
PERF102.py:23:26: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:20:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
23 | for (key1, _), (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
24 | pass
19 | def f():
20 | for name, (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
21 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
20 20 | pass
21 21 |
17 17 |
18 18 |
19 19 | def f():
20 |- for name, (_, _) in some_dict.items(): # PERF102
20 |+ for name in some_dict.keys(): # PERF102
21 21 | print(name)
22 22 |
23 |-for (key1, _), (_, _) in some_dict.items(): # PERF102
23 |+for (key1, _) in some_dict.keys(): # PERF102
24 24 | pass
25 25 |
26 26 |
23 23 |
PERF102.py:27:32: PERF102 [*] When using only the values of a dict use the `values()` method
PERF102.py:30:30: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
27 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
28 | pass
29 | def f():
30 | for (key1, _), (_, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
31 | print(key1)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
27 27 |
28 28 |
29 29 | def f():
30 |- for (key1, _), (_, _) in some_dict.items(): # PERF102
30 |+ for (key1, _) in some_dict.keys(): # PERF102
31 31 | print(key1)
32 32 |
33 33 |
PERF102.py:35:36: PERF102 [*] When using only the values of a dict use the `values()` method
|
34 | def f():
35 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
36 | print(value)
|
= help: Replace `.items()` with `.values()`
Suggested fix
24 24 | pass
25 25 |
26 26 |
27 |-for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
27 |+for (value, _) in some_dict.values(): # PERF102
28 28 | pass
29 29 |
30 30 |
PERF102.py:39:28: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
39 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
40 | pass
|
= help: Replace `.items()` with `.keys()`
Suggested fix
36 36 | pass
32 32 |
33 33 |
34 34 | def f():
35 |- for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
35 |+ for (value, _) in some_dict.values(): # PERF102
36 36 | print(value)
37 37 |
38 38 |
39 |-for ((_, key2), (_, _)) in some_dict.items(): # PERF102
39 |+for (_, key2) in some_dict.keys(): # PERF102
40 40 | pass
41 41 |
42 42 |
PERF102.py:67:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:50:32: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
67 | for name, (_, _) in (some_function()).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^ PERF102
68 | pass
49 | def f():
50 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
51 | print(key2)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
64 64 | print(value)
65 65 |
66 66 |
67 |-for name, (_, _) in (some_function()).items(): # PERF102
67 |+for name in (some_function()).keys(): # PERF102
68 68 | pass
69 69 |
70 70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
47 47 |
48 48 |
49 49 | def f():
50 |- for ((_, key2), (_, _)) in some_dict.items(): # PERF102
50 |+ for (_, key2) in some_dict.keys(): # PERF102
51 51 | print(key2)
52 52 |
53 53 |
PERF102.py:70:21: PERF102 [*] When using only the keys of a dict use the `keys()` method
PERF102.py:85:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
68 | pass
69 |
70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102
71 | pass
84 | def f():
85 | for name, (_, _) in (some_function()).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^ PERF102
86 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
67 67 | for name, (_, _) in (some_function()).items(): # PERF102
68 68 | pass
69 69 |
70 |-for name, (_, _) in (some_function().some_attribute).items(): # PERF102
70 |+for name in (some_function().some_attribute).keys(): # PERF102
71 71 | pass
82 82 |
83 83 |
84 84 | def f():
85 |- for name, (_, _) in (some_function()).items(): # PERF102
85 |+ for name in (some_function()).keys(): # PERF102
86 86 | print(name)
87 87 |
88 88 |
PERF102.py:90:25: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
89 | def f():
90 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102
91 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
87 87 |
88 88 |
89 89 | def f():
90 |- for name, (_, _) in (some_function().some_attribute).items(): # PERF102
90 |+ for name in (some_function().some_attribute).keys(): # PERF102
91 91 | print(name)
92 92 |
93 93 |
PERF102.py:95:31: PERF102 [*] When using only the keys of a dict use the `keys()` method
|
94 | def f():
95 | for name, unused_value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
96 | print(name)
|
= help: Replace `.items()` with `.keys()`
Suggested fix
92 92 |
93 93 |
94 94 | def f():
95 |- for name, unused_value in some_dict.items(): # PERF102
95 |+ for name in some_dict.keys(): # PERF102
96 96 | print(name)
97 97 |
98 98 |
PERF102.py:100:31: PERF102 [*] When using only the values of a dict use the `values()` method
|
99 | def f():
100 | for unused_name, value in some_dict.items(): # PERF102
| ^^^^^^^^^^^^^^^ PERF102
101 | print(value)
|
= help: Replace `.items()` with `.values()`
Suggested fix
97 97 |
98 98 |
99 99 | def f():
100 |- for unused_name, value in some_dict.items(): # PERF102
100 |+ for value in some_dict.values(): # PERF102
101 101 | print(value)

View File

@@ -53,16 +53,13 @@ pub(super) fn is_overlong(
task_tags: &[String],
tab_size: TabSize,
) -> Option<Overlong> {
let mut start_offset = line.start();
let mut width = LineWidth::new(tab_size);
for c in line.chars() {
if width < limit {
start_offset += c.text_len();
}
width = width.add_char(c);
// Each character is between 1-4 bytes. If the number of bytes is smaller than the limit, it cannot be overlong.
if line.len() < limit.get() {
return None;
}
let mut width = LineWidth::new(tab_size);
width = width.add_str(line.as_str());
if width <= limit {
return None;
}
@@ -91,6 +88,17 @@ pub(super) fn is_overlong(
}
}
// Obtain the start offset of the part of the line that exceeds the limit
let mut start_offset = line.start();
let mut start_width = LineWidth::new(tab_size);
for c in line.chars() {
if start_width < limit {
start_offset += c.text_len();
start_width = start_width.add_char(c);
} else {
break;
}
}
Some(Overlong {
range: TextRange::new(start_offset, line.end()),
width: width.get(),

View File

@@ -1,4 +1,4 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{Identifier, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -35,14 +35,11 @@ impl Violation for AmbiguousClassName {
}
/// E742
pub(crate) fn ambiguous_class_name<F>(name: &str, locate: F) -> Option<Diagnostic>
where
F: FnOnce() -> TextRange,
{
pub(crate) fn ambiguous_class_name(name: &Identifier) -> Option<Diagnostic> {
if is_ambiguous_name(name) {
Some(Diagnostic::new(
AmbiguousClassName(name.to_string()),
locate(),
name.range(),
))
} else {
None

View File

@@ -1,4 +1,4 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{Identifier, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -35,14 +35,11 @@ impl Violation for AmbiguousFunctionName {
}
/// E743
pub(crate) fn ambiguous_function_name<F>(name: &str, locate: F) -> Option<Diagnostic>
where
F: FnOnce() -> TextRange,
{
pub(crate) fn ambiguous_function_name(name: &Identifier) -> Option<Diagnostic> {
if is_ambiguous_name(name) {
Some(Diagnostic::new(
AmbiguousFunctionName(name.to_string()),
locate(),
name.range(),
))
} else {
None

View File

@@ -225,6 +225,7 @@ fn function(
decorator_list: vec![],
returns: Some(Box::new(return_type)),
type_comment: None,
type_params: vec![],
range: TextRange::default(),
});
return generator.stmt(&func);
@@ -237,6 +238,7 @@ fn function(
decorator_list: vec![],
returns: None,
type_comment: None,
type_params: vec![],
range: TextRange::default(),
});
generator.stmt(&func)

View File

@@ -1187,19 +1187,22 @@ impl AlwaysAutofixableViolation for SectionNameEndsInColon {
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
#[violation]
pub struct UndocumentedParam {
pub names: Vec<String>,
/// The name of the function being documented.
definition: String,
/// The names of the undocumented parameters.
names: Vec<String>,
}
impl Violation for UndocumentedParam {
#[derive_message_formats]
fn message(&self) -> String {
let UndocumentedParam { names } = self;
let UndocumentedParam { definition, names } = self;
if names.len() == 1 {
let name = &names[0];
format!("Missing argument description in the docstring: `{name}`")
format!("Missing argument description in the docstring for `{definition}`: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Missing argument descriptions in the docstring: {names}")
format!("Missing argument descriptions in the docstring for `{definition}`: {names}")
}
}
}
@@ -1779,11 +1782,16 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
}
if !missing_arg_names.is_empty() {
let names = missing_arg_names.into_iter().sorted().collect();
checker.diagnostics.push(Diagnostic::new(
UndocumentedParam { names },
stmt.identifier(),
));
if let Some(definition) = docstring.definition.name() {
let names = missing_arg_names.into_iter().sorted().collect();
checker.diagnostics.push(Diagnostic::new(
UndocumentedParam {
definition: definition.to_string(),
names,
},
stmt.identifier(),
));
}
}
}

View File

@@ -87,6 +87,32 @@ pub struct Options {
)]
/// Whether to use Google-style or NumPy-style conventions or the PEP257
/// defaults when analyzing docstring sections.
///
/// Enabling a convention will force-disable any rules that are not
/// included in the specified convention. As such, the intended use is
/// to enable a convention and then selectively disable any additional
/// rules on top of it.
///
/// For example, to use Google-style conventions but avoid requiring
/// documentation for every function parameter:
///
/// ```toml
/// [tool.ruff]
/// # Enable all `pydocstyle` rules, limiting to those that adhere to the
/// # Google convention via `convention = "google"`, below.
/// select = ["D"]
///
/// # On top of the Google convention, disable `D417`, which requires
/// # documentation for every function parameter.
/// ignore = ["D417"]
///
/// [tool.ruff.pydocstyle]
/// convention = "google"
/// ```
///
/// As conventions force-disable all rules not included in the convention,
/// enabling _additional_ rules on top of a convention is currently
/// unsupported.
pub convention: Option<Convention>,
#[option(
default = r#"[]"#,

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
sections.py:292:9: D417 Missing argument description in the docstring: `y`
sections.py:292:9: D417 Missing argument description in the docstring for `bar`: `y`
|
290 | x = 1
291 |
@@ -10,7 +10,7 @@ sections.py:292:9: D417 Missing argument description in the docstring: `y`
293 | """Nested function test for docstrings.
|
sections.py:309:5: D417 Missing argument description in the docstring: `y`
sections.py:309:5: D417 Missing argument description in the docstring for `test_missing_google_args`: `y`
|
307 | "(argument(s) y are missing descriptions in "
308 | "'test_missing_google_args' docstring)")
@@ -19,7 +19,7 @@ sections.py:309:5: D417 Missing argument description in the docstring: `y`
310 | """Toggle the gizmo.
|
sections.py:333:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
sections.py:333:9: D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `y`, `z`
|
331 | "(argument(s) test, y, z are missing descriptions in "
332 | "'test_missing_args' docstring)", arg_count=5)
@@ -28,7 +28,7 @@ sections.py:333:9: D417 Missing argument descriptions in the docstring: `test`,
334 | """Test a valid args section.
|
sections.py:345:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
sections.py:345:9: D417 Missing argument descriptions in the docstring for `test_missing_args_class_method`: `test`, `y`, `z`
|
343 | "(argument(s) test, y, z are missing descriptions in "
344 | "'test_missing_args_class_method' docstring)", arg_count=5)
@@ -37,7 +37,7 @@ sections.py:345:9: D417 Missing argument descriptions in the docstring: `test`,
346 | """Test a valid args section.
|
sections.py:358:9: D417 Missing argument descriptions in the docstring: `a`, `y`, `z`
sections.py:358:9: D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `y`, `z`
|
356 | "(argument(s) a, y, z are missing descriptions in "
357 | "'test_missing_args_static_method' docstring)", arg_count=4)
@@ -46,7 +46,7 @@ sections.py:358:9: D417 Missing argument descriptions in the docstring: `a`, `y`
359 | """Test a valid args section.
|
sections.py:370:9: D417 Missing argument descriptions in the docstring: `a`, `b`
sections.py:370:9: D417 Missing argument descriptions in the docstring for `test_missing_docstring`: `a`, `b`
|
368 | "(argument(s) a, b are missing descriptions in "
369 | "'test_missing_docstring' docstring)", arg_count=2)
@@ -55,7 +55,7 @@ sections.py:370:9: D417 Missing argument descriptions in the docstring: `a`, `b`
371 | """Test a valid args section.
|
sections.py:398:5: D417 Missing argument description in the docstring: `y`
sections.py:398:5: D417 Missing argument description in the docstring for `test_missing_numpy_args`: `y`
|
396 | "(argument(s) y are missing descriptions in "
397 | "'test_missing_numpy_args' docstring)")
@@ -64,7 +64,7 @@ sections.py:398:5: D417 Missing argument description in the docstring: `y`
399 | """Toggle the gizmo.
|
sections.py:434:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
sections.py:434:9: D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `y`, `z`
|
432 | "(argument(s) test, y, z are missing descriptions in "
433 | "'test_missing_args' docstring)", arg_count=5)
@@ -73,7 +73,7 @@ sections.py:434:9: D417 Missing argument descriptions in the docstring: `test`,
435 | """Test a valid args section.
|
sections.py:449:9: D417 Missing argument descriptions in the docstring: `test`, `y`, `z`
sections.py:449:9: D417 Missing argument descriptions in the docstring for `test_missing_args_class_method`: `test`, `y`, `z`
|
447 | "(argument(s) test, y, z are missing descriptions in "
448 | "'test_missing_args_class_method' docstring)", arg_count=4)
@@ -82,7 +82,7 @@ sections.py:449:9: D417 Missing argument descriptions in the docstring: `test`,
450 | """Test a valid args section.
|
sections.py:468:9: D417 Missing argument descriptions in the docstring: `a`, `z`
sections.py:468:9: D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `z`
|
466 | "(argument(s) a, z are missing descriptions in "
467 | "'test_missing_args_static_method' docstring)", arg_count=3)
@@ -91,7 +91,7 @@ sections.py:468:9: D417 Missing argument descriptions in the docstring: `a`, `z`
469 | """Test a valid args section.
|
sections.py:498:9: D417 Missing argument description in the docstring: `y`
sections.py:498:9: D417 Missing argument description in the docstring for `test_incorrect_indent`: `y`
|
496 | "(argument(s) y are missing descriptions in "
497 | "'test_incorrect_indent' docstring)", arg_count=3)

View File

@@ -1,63 +1,63 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
D417.py:1:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:1:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
1 | def f(x, y, z):
| ^ D417
2 | """Do something.
|
D417.py:14:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:14:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
14 | def f(x, y, z):
| ^ D417
15 | """Do something.
|
D417.py:27:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:27:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
27 | def f(x, y, z):
| ^ D417
28 | """Do something.
|
D417.py:39:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:39:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
39 | def f(x, y, z):
| ^ D417
40 | """Do something.
|
D417.py:52:5: D417 Missing argument description in the docstring: `y`
D417.py:52:5: D417 Missing argument description in the docstring for `f`: `y`
|
52 | def f(x, y, z):
| ^ D417
53 | """Do something.
|
D417.py:65:5: D417 Missing argument description in the docstring: `y`
D417.py:65:5: D417 Missing argument description in the docstring for `f`: `y`
|
65 | def f(x, y, z):
| ^ D417
66 | """Do something.
|
D417.py:77:5: D417 Missing argument description in the docstring: `y`
D417.py:77:5: D417 Missing argument description in the docstring for `f`: `y`
|
77 | def f(x, y, z):
| ^ D417
78 | """Do something.
|
D417.py:98:5: D417 Missing argument description in the docstring: `x`
D417.py:98:5: D417 Missing argument description in the docstring for `f`: `x`
|
98 | def f(x, *args, **kwargs):
| ^ D417
99 | """Do something.
|
D417.py:108:5: D417 Missing argument description in the docstring: `*args`
D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*args`
|
108 | def f(x, *args, **kwargs):
| ^ D417

View File

@@ -1,63 +1,63 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
D417.py:1:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:1:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
1 | def f(x, y, z):
| ^ D417
2 | """Do something.
|
D417.py:14:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:14:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
14 | def f(x, y, z):
| ^ D417
15 | """Do something.
|
D417.py:27:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:27:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
27 | def f(x, y, z):
| ^ D417
28 | """Do something.
|
D417.py:39:5: D417 Missing argument descriptions in the docstring: `y`, `z`
D417.py:39:5: D417 Missing argument descriptions in the docstring for `f`: `y`, `z`
|
39 | def f(x, y, z):
| ^ D417
40 | """Do something.
|
D417.py:52:5: D417 Missing argument description in the docstring: `y`
D417.py:52:5: D417 Missing argument description in the docstring for `f`: `y`
|
52 | def f(x, y, z):
| ^ D417
53 | """Do something.
|
D417.py:65:5: D417 Missing argument description in the docstring: `y`
D417.py:65:5: D417 Missing argument description in the docstring for `f`: `y`
|
65 | def f(x, y, z):
| ^ D417
66 | """Do something.
|
D417.py:77:5: D417 Missing argument description in the docstring: `y`
D417.py:77:5: D417 Missing argument description in the docstring for `f`: `y`
|
77 | def f(x, y, z):
| ^ D417
78 | """Do something.
|
D417.py:98:5: D417 Missing argument description in the docstring: `x`
D417.py:98:5: D417 Missing argument description in the docstring for `f`: `x`
|
98 | def f(x, *args, **kwargs):
| ^ D417
99 | """Do something.
|
D417.py:108:5: D417 Missing argument description in the docstring: `*args`
D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*args`
|
108 | def f(x, *args, **kwargs):
| ^ D417

View File

@@ -33,7 +33,6 @@ pub(crate) fn break_outside_loop<'a>(
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) -> Option<Diagnostic> {
let mut allowed: bool = false;
let mut child = stmt;
for parent in parents {
match parent {
@@ -41,8 +40,7 @@ pub(crate) fn break_outside_loop<'a>(
| Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
allowed = true;
break;
return None;
}
}
Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => {
@@ -53,9 +51,5 @@ pub(crate) fn break_outside_loop<'a>(
child = parent;
}
if allowed {
None
} else {
Some(Diagnostic::new(BreakOutsideLoop, stmt.range()))
}
Some(Diagnostic::new(BreakOutsideLoop, stmt.range()))
}

View File

@@ -33,7 +33,6 @@ pub(crate) fn continue_outside_loop<'a>(
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) -> Option<Diagnostic> {
let mut allowed: bool = false;
let mut child = stmt;
for parent in parents {
match parent {
@@ -41,8 +40,7 @@ pub(crate) fn continue_outside_loop<'a>(
| Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
allowed = true;
break;
return None;
}
}
Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => {
@@ -53,9 +51,5 @@ pub(crate) fn continue_outside_loop<'a>(
child = parent;
}
if allowed {
None
} else {
Some(Diagnostic::new(ContinueOutsideLoop, stmt.range()))
}
Some(Diagnostic::new(ContinueOutsideLoop, stmt.range()))
}

View File

@@ -1,5 +1,3 @@
use rustpython_parser::ast::Ranged;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -55,21 +53,14 @@ impl Violation for GlobalStatement {
/// PLW0603
pub(crate) fn global_statement(checker: &mut Checker, name: &str) {
let scope = checker.semantic().scope();
if let Some(binding_id) = scope.get(name) {
let binding = checker.semantic().binding(binding_id);
if binding.is_global() {
if let Some(source) = binding.source {
let source = checker.semantic().stmts[source];
checker.diagnostics.push(Diagnostic::new(
GlobalStatement {
name: name.to_string(),
},
// Match Pylint's behavior by reporting on the `global` statement`, rather
// than the variable usage.
source.range(),
));
}
}
if let Some(range) = checker.semantic().global(name) {
checker.diagnostics.push(Diagnostic::new(
GlobalStatement {
name: name.to_string(),
},
// Match Pylint's behavior by reporting on the `global` statement`, rather
// than the variable usage.
range,
));
}
}

View File

@@ -82,7 +82,6 @@ fn is_magic_value(constant: &Constant, allowed_types: &[ConstantType]) -> bool {
Constant::Str(value) => !matches!(value.as_str(), "" | "__main__"),
Constant::Int(value) => !matches!(value.try_into(), Ok(0 | 1)),
Constant::Bytes(_) => true,
Constant::Tuple(_) => true,
Constant::Float(_) => true,
Constant::Complex { .. } => true,
}

View File

@@ -150,7 +150,7 @@ pub(crate) fn nested_min_max(
}) {
let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range());
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(expr, checker.locator) {
if !has_comments(expr, checker.locator, checker.indexer) {
let flattened_expr = Expr::Call(ast::ExprCall {
func: Box::new(func.clone()),
args: collect_nested_args(min_max, args, checker.semantic()),

View File

@@ -28,7 +28,6 @@ impl TryFrom<&Constant> for ConstantType {
Constant::Float(..) => Ok(Self::Float),
Constant::Int(..) => Ok(Self::Int),
Constant::Str(..) => Ok(Self::Str),
Constant::Tuple(..) => Ok(Self::Tuple),
Constant::Bool(..) | Constant::Ellipsis | Constant::None => {
Err(anyhow!("Singleton constants are unsupported"))
}

View File

@@ -89,12 +89,13 @@ global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT
82 | CONSTANT = 2
|
global_statement.py:81:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
|
78 | def multiple_assignment():
79 | """Should warn on every assignment."""
80 | global CONSTANT # [global-statement]
| ^^^^^^^^^^^^^^^ PLW0603
81 | CONSTANT = 1
| ^^^^^^^^^^^^ PLW0603
82 | CONSTANT = 2
|

View File

@@ -93,11 +93,7 @@ fn match_named_tuple_assign<'a>(
/// Generate a `Stmt::AnnAssign` representing the provided property
/// definition.
fn create_property_assignment_stmt(
property: &str,
annotation: &Expr,
value: Option<&Expr>,
) -> Stmt {
fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt {
ast::StmtAnnAssign {
target: Box::new(
ast::ExprName {
@@ -108,40 +104,15 @@ fn create_property_assignment_stmt(
.into(),
),
annotation: Box::new(annotation.clone()),
value: value.map(|value| Box::new(value.clone())),
value: None,
simple: true,
range: TextRange::default(),
}
.into()
}
/// Match the `defaults` keyword in a `NamedTuple(...)` call.
fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> {
let defaults = keywords.iter().find(|keyword| {
if let Some(arg) = &keyword.arg {
arg == "defaults"
} else {
false
}
});
match defaults {
Some(defaults) => match &defaults.value {
Expr::List(ast::ExprList { elts, .. }) => Ok(elts),
Expr::Tuple(ast::ExprTuple { elts, .. }) => Ok(elts),
_ => bail!("Expected defaults to be `Expr::List` | `Expr::Tuple`"),
},
None => Ok(&[]),
}
}
/// Create a list of property assignments from the `NamedTuple` arguments.
fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<Stmt>> {
let Some(fields) = args.get(1) else {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
});
return Ok(vec![node]);
};
/// Create a list of property assignments from the `NamedTuple` fields argument.
fn create_properties_from_fields_arg(fields: &Expr) -> Result<Vec<Stmt>> {
let Expr::List(ast::ExprList { elts, .. }) = &fields else {
bail!("Expected argument to be `Expr::List`");
};
@@ -151,16 +122,8 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
});
return Ok(vec![node]);
}
let padded_defaults = if elts.len() >= defaults.len() {
std::iter::repeat(None)
.take(elts.len() - defaults.len())
.chain(defaults.iter().map(Some))
} else {
bail!("Defaults must be `None` or an iterable of at least the number of fields")
};
elts.iter()
.zip(padded_defaults)
.map(|(field, default)| {
.map(|field| {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &field else {
bail!("Expected `field` to be `Expr::Tuple`")
};
@@ -180,9 +143,21 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
if is_dunder(property) {
bail!("Cannot use dunder property name: {}", property)
}
Ok(create_property_assignment_stmt(
property, annotation, default,
))
Ok(create_property_assignment_stmt(property, annotation))
})
.collect()
}
/// Create a list of property assignments from the `NamedTuple` keyword arguments.
fn create_properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
keywords
.iter()
.map(|keyword| {
let Keyword { arg, value, .. } = keyword;
let Some(arg) = arg else {
bail!("Expected `keyword` to have an `arg`")
};
Ok(create_property_assignment_stmt(arg.as_str(), value))
})
.collect()
}
@@ -196,6 +171,7 @@ fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &Expr) ->
keywords: vec![],
body,
decorator_list: vec![],
type_params: vec![],
range: TextRange::default(),
}
.into()
@@ -228,12 +204,32 @@ pub(crate) fn convert_named_tuple_functional_to_class(
return;
};
let properties = match match_defaults(keywords)
.and_then(|defaults| create_properties_from_args(args, defaults))
{
Ok(properties) => properties,
Err(err) => {
debug!("Skipping `NamedTuple` \"{typename}\": {err}");
let properties = match (&args[1..], keywords) {
// Ex) NamedTuple("MyType")
([], []) => vec![Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
})],
// Ex) NamedTuple("MyType", [("a", int), ("b", str)])
([fields], []) => {
if let Ok(properties) = create_properties_from_fields_arg(fields) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse fields");
return;
}
}
// Ex) NamedTuple("MyType", a=int, b=str)
([], keywords) => {
if let Ok(properties) = create_properties_from_keywords(keywords) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse keywords");
return;
}
}
// Unfixable
_ => {
debug!("Skipping `NamedTuple` \"{typename}\": mixed fields and keywords");
return;
}
};

View File

@@ -128,6 +128,7 @@ fn create_class_def_stmt(
keywords,
body,
decorator_list: vec![],
type_params: vec![],
range: TextRange::default(),
}
.into()

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