Compare commits

...

44 Commits

Author SHA1 Message Date
Charlie Marsh
a0afde1a3f Avoid token clone in implicit string rule 2024-02-05 22:14:29 -05:00
Seo Sanghyeon
df7fb95cbc Index multiline f-strings (#9837)
Fix #9777.
2024-02-05 21:25:33 -05:00
Adrian
83195a6030 ruff-ecosystem: Add indico/indico repo (#9850)
It's a pretty big codebase using lots of different stuff, so a good
candidate for finding obscure problems.

I didn't look more closely which options are used (I have the feeling
`--select ALL` is not implied, since I see you adding it via
`check_options` for certain entries but not for others), the repo itself
has a pretty large ruff.toml - but assuming ecosystem just cares about
differences between base and head of a PR, `ALL` most likely makes
sense.
2024-02-06 00:37:58 +00:00
Daniël van Noord
d31d09d7cd Add `--preview` to instruction for running newly added tests (#9846)
## Summary

This surprised me while working on adding a test. I thought about adding
an additional `note`, but how often is this incorrect? In general,
people reading the contributing guidelines probably want to enable this
flag and those who don't will know enough about the testing setup to
have their own commands/aliases.

## Test Plan

Ran CI on local fork and got an all green.
2024-02-05 19:33:22 -05:00
Tyler C Laprade, CFA
0f436b71f3 Typo in 0.2.1 changelog (#9847)
`refurn` -> `refurb`
2024-02-05 17:51:27 -05:00
Eero Vaher
cd5bcd815d Mention a related setting in C408 description (#9839)
#2977 added the `allow-dict-calls-with-keyword-arguments` configuration
option for the `unnecessary-collection-call (C408)` rule, but it did not
update the rule description.
2024-02-06 03:57:53 +05:30
Charlie Marsh
0ccca4083a Bump version to v0.2.1 (#9843) 2024-02-05 15:31:05 -05:00
Charlie Marsh
041ce1e166 Respect generic Protocol in ellipsis removal (#9841)
Closes https://github.com/astral-sh/ruff/issues/9840.
2024-02-05 19:36:16 +00:00
Dhruv Manilawala
36b752876e Implement AnyNode/AnyNodeRef for FStringFormatSpec (#9836)
## Summary

This PR adds the `AnyNode` and `AnyNodeRef` implementation for
`FStringFormatSpec` node which will be required in the f-string
formatting.

The main usage for this is so that we can pass in the node directly to
`suppressed_node` in case debug expression is used to format is as
verbatim text.
2024-02-05 19:23:43 +00:00
Micha Reiser
b3dc565473 Add --range option to ruff format (#9733)
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
2024-02-05 19:21:45 +00:00
Thomas M Kehrenberg
e708c08b64 Fix default for max-positional-args (#9838)
<!--
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
`max-positional-args` defaults to `max-args` if it's not specified and
the default to `max-args` is 5, so saying that the default is 3 is
definitely wrong. Ideally, we wouldn't specify a default at all for this
config option, but I don't think that's possible?

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

## Test Plan

<!-- How was it tested? -->
Not sure.
2024-02-05 16:58:14 +00:00
Charlie Marsh
73902323d5 Revert "Use publicly available Apple Silicon runners (#9726)" (#9834)
## Summary

Sadly, the Apple Silicon runners use macOS 14 and produce binaries that
segfault when run on macOS 11 (at least), and possibly on macOS 12
and/or macOS 13.

macOS 11 is EOL, but it doesn't seem like a good tradeoff to speed up
our release builds at the expense of user support and compatibility.

This reverts commit f0066e1b89.

Closes https://github.com/astral-sh/ruff/issues/9823.
2024-02-05 11:24:51 -05:00
Charlie Marsh
9781563ef6 Add fast-path for comment detection (#9808)
## Summary

When we fall through to parsing, the comment-detection rule is a
significant portion of lint time. This PR adds an additional fast
heuristic whereby we abort if a comment contains two consecutive name
tokens (via the zero-allocation lexer). For the `ctypeslib.py`, which
has a few cases that are now caught by this, it's a 2.5x speedup for the
rule (and a 20% speedup for token-based rules).
2024-02-05 11:00:18 -05:00
Zanie Blue
84aea7f0c8 Drop __get__ and __set__ from unnecessary-dunder-call (#9791)
These are for descriptors which affects the behavior of the object _as a
property_; I do not think they should be called directly but there is no
alternative when working with the object directly.

Closes https://github.com/astral-sh/ruff/issues/9789
2024-02-05 10:54:29 -05:00
Shaygan Hooshyari
b47f85eb69 Preview Style: Format module level docstring (#9725)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-05 15:03:34 +00:00
Micha Reiser
80fc02e7d5 Don't trim last empty line in docstrings (#9813) 2024-02-05 13:29:24 +00:00
dependabot[bot]
55d0e1148c Bump memchr from 2.6.4 to 2.7.1 (#9827)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 13:24:23 +00:00
dependabot[bot]
1de945e3eb Bump is-macro from 0.3.4 to 0.3.5 (#9829) 2024-02-05 13:11:15 +00:00
dependabot[bot]
e277ba20da Bump pyproject-toml from 0.8.1 to 0.8.2 (#9826) 2024-02-05 14:05:52 +01:00
dependabot[bot]
2e836a4cbe Bump toml from 0.8.8 to 0.8.9 (#9828) 2024-02-05 14:05:27 +01:00
dependabot[bot]
57d6cdb8d3 Bump itertools from 0.12.0 to 0.12.1 (#9830) 2024-02-05 14:03:06 +01:00
Charlie Marsh
602f8b8250 Remove CST-based fixer for C408 (#9822)
## Summary

We have to keep the fixer for a specific case: `dict` calls that include
keyword-argument members.
2024-02-04 22:26:51 -05:00
Charlie Marsh
a6bc4b2e48 Remove CST-based fixers for C405 and C409 (#9821) 2024-02-05 02:17:34 +00:00
Charlie Marsh
c5fa0ccffb Remove CST-based fixers for C400, C401, C410, and C418 (#9819) 2024-02-04 21:00:11 -05:00
Charlie Marsh
dd77d29d0e Remove LibCST-based fixer for C403 (#9818)
## Summary

Experimenting with rewriting one of the comprehension fixes _without_
LibCST.
2024-02-04 20:08:19 -05:00
Charlie Marsh
ad0121660e Run dunder method rule on methods directly (#9815)
This stood out in the flamegraph and I realized it requires us to
traverse over all statements in the class (unnecessarily).
2024-02-04 14:24:57 -05:00
Charlie Marsh
5c99967c4d Short-circuit typing matches based on imports (#9800) 2024-02-04 14:06:44 -05:00
Charlie Marsh
c53aae0b6f Add our own ignored-names abstractions (#9802)
## Summary

These run over nearly every identifier. It's rare to override them, so
when not provided, we can just use a match against the hardcoded default
set.
2024-02-03 09:48:07 -05:00
Charlie Marsh
2352de2277 Slight speed-up for lowercase and uppercase identifier checks (#9798)
It turns out that for ASCII identifiers, this is nearly 2x faster:

```
Parser/before     time:   [15.388 ns 15.395 ns 15.406 ns]
Parser/after      time:   [8.3786 ns 8.5821 ns 8.7715 ns]
```
2024-02-03 14:40:41 +00:00
Jane Lewis
e0a6034cbb Implement RUF027: Missing F-String Syntax lint (#9728)
<!--
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

Fixes #8151

This PR implements a new rule, `RUF027`.

## What it does
Checks for strings that contain f-string syntax but are not f-strings.

### Why is this bad?
An f-string missing an `f` at the beginning won't format anything, and
instead treat the interpolation syntax as literal.

### Example

```python
name = "Sarah"
dayofweek = "Tuesday"
msg = "Hello {name}! It is {dayofweek} today!"
```

It should instead be:
```python
name = "Sarah"
dayofweek = "Tuesday"
msg = f"Hello {name}! It is {dayofweek} today!"
```

## Heuristics
Since there are many possible string literals which contain syntax
similar to f-strings yet are not intended to be,
this lint will disqualify any literal that satisfies any of the
following conditions:
1. The string literal is a standalone expression. For example, a
docstring.
2. The literal is part of a function call with keyword arguments that
match at least one variable (for example: `format("Message: {value}",
value = "Hello World")`)
3. The literal (or a parent expression of the literal) has a direct
method call on it (for example: `"{value}".format(...)`)
4. The string has no `{...}` expression sections, or uses invalid
f-string syntax.
5. The string references variables that are not in scope, or it doesn't
capture variables at all.
6. Any format specifiers in the potential f-string are invalid.

## Test Plan

I created a new test file, `RUF027.py`, which is both an example of what
the lint should catch and a way to test edge cases that may trigger
false positives.
2024-02-03 00:21:03 +00:00
Emil Telstad
25d93053da Update max-pos-args example to max-positional-args. (#9797) 2024-02-02 20:29:13 +00:00
Charlie Marsh
ee5b07d4ca Skip empty lines when determining base indentation (#9795)
## Summary

It turns out we saw a panic in cases when dedenting blocks like the `def
wrapper` here:

```python
def instrument_url(f: UrlFuncT) -> UrlFuncT:
    # TODO: Type this with ParamSpec to preserve the function signature.
    if not INSTRUMENTING:  # nocoverage -- option is always enabled; should we remove?
        return f
    else:

        def wrapper(
            self: "ZulipTestCase", url: str, info: object = {}, **kwargs: Union[bool, str]
        ) -> HttpResponseBase:
```

Since we relied on the first line to determine the indentation, instead
of the first non-empty line.

## Test Plan

`cargo test`
2024-02-02 19:42:47 +00:00
Charlie Marsh
e50603caf6 Track top-level module imports in the semantic model (#9775)
## Summary

This is a simple idea to avoid unnecessary work in the linter,
especially for rules that run on all name and/or all attribute nodes.
Imagine a rule like the NumPy deprecation check. If the user never
imported `numpy`, we should be able to skip that rule entirely --
whereas today, we do a `resolve_call_path` check on _every_ name in the
file. It turns out that there's basically a finite set of modules that
we care about, so we now track imports on those modules as explicit
flags on the semantic model. In rules that can _only_ ever trigger if
those modules were imported, we add a dedicated and extremely cheap
check to the top of the rule.

We could consider generalizing this to all modules, but I would expect
that not to be much faster than `resolve_call_path`, which is just a
hash map lookup on `TextSize` anyway.

It would also be nice to make this declarative, such that rules could
declare the modules they care about, the analyzers could call the rules
as appropriate. But, I don't think such a design should block merging
this.
2024-02-02 14:37:20 -05:00
Charlie Marsh
c3ca34543f Skip LibCST parsing for standard dedent adjustments (#9769)
## Summary

Often, when fixing, we need to dedent a block of code (e.g., if we
remove an `if` and dedent its body). Today, we use LibCST to parse and
adjust the indentation, which is really expensive -- but this is only
really necessary if the block contains a multiline string, since naively
adjusting the indentation for such a string can change the whitespace
_within_ the string.

This PR uses a simple dedent implementation for cases in which the block
doesn't intersect with a multi-line string (or an f-string, since we
don't support tracking multi-line strings for f-strings right now).

We could improve this even further by using the ranges to guide the
dedent function, such that we don't apply the dedent if the line starts
within a multiline string. But that would also need to take f-strings
into account, which is a little tricky.

## Test Plan

`cargo test`
2024-02-02 18:13:46 +00:00
Micha Reiser
4f7fb566f0 Range formatting: Fix invalid syntax after parenthesizing expression (#9751) 2024-02-02 17:56:25 +01:00
Jordan Danford
50bfbcf568 README.md: add missing "your" in support section, add alt text to Astral logo (#9787) 2024-02-02 09:09:19 -06:00
Charlie Marsh
ea1c089652 Use AhoCorasick to speed up quote match (#9773)
<!--
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

When I was looking at the v0.2.0 release, this method showed up in a
CodSpeed regression (we were calling it more), so I decided to quickly
look at speeding it up. @BurntSushi suggested using Aho-Corasick, and it
looks like it's about 7 or 8x faster:

```text
Parser/AhoCorasick      time:   [8.5646 ns 8.5914 ns 8.6191 ns]
Parser/Iterator         time:   [64.992 ns 65.124 ns 65.271 ns]
```

## Test Plan

`cargo test`
2024-02-02 09:57:39 -05:00
Mikael Arguedas
b947dde8ad [flake8-bugbear][B006] remove outdated comment (#9776)
I noticed that the comment doesn't match the behavior:
- zip function is not used anymore
- parameters are not scanned in reverse

## Summary

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

## Test Plan

No need

Signed-off-by: Mikael Arguedas <mikael.arguedas@gmail.com>
2024-02-02 09:32:46 -05:00
trag1c
d259cd0d32 Made hyperlink on homepage correctly redirect to GitHub (#9784)
## Summary

Closes #9783. Feels hacky because of the different key so there *might*
be a nicer way to do this 😄

## Test Plan
Tested locally with `mkdocs serve`.
2024-02-02 09:32:23 -05:00
Thomas Grainger
af4db39205 Add pytest to who's using ruff (#9782) 2024-02-02 11:26:30 +00:00
Alex Gaynor
467c091382 Fixed example code in weak_cryptographic_key.rs (#9774)
The proper way to use these APIs is to instantiate the curve classes
2024-02-01 22:42:31 -05:00
Charlie Marsh
92d99a72d9 Fix references to deprecated ANN rules in changelog (#9771)
We deprecated `ANN101` and `ANN102`, but the changelog says `ANN001` and
`ANN002`. (I've confirmed that the code reflects the correct
deprecation; it's just a documentation error.)
2024-02-02 02:27:30 +00:00
Charlie Marsh
66d2c1e1c4 Move adjust_indentation to a shared home (#9768)
Now that this method is used in multiple linters, it should be moved out
of the `pyupgrade` module.
2024-02-02 00:53:59 +00:00
Charlie Marsh
ded8c7629f Invert order of checks in zero-sleep-call (#9766)
The other conditions are cheaper and should eliminate the vast majority
of these checks.
2024-02-01 23:30:03 +00:00
227 changed files with 4397 additions and 1530 deletions

View File

@@ -58,7 +58,7 @@ jobs:
path: dist
macos-x86_64:
runs-on: macos-14
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
@@ -74,6 +74,11 @@ jobs:
with:
target: x86_64
args: --release --locked --out dist
- name: "Test wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
@@ -93,7 +98,7 @@ jobs:
*.sha256
macos-universal:
runs-on: macos-14
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:

View File

@@ -1,5 +1,50 @@
# Changelog
## 0.2.1
This release includes support for range formatting (i.e., the ability to format specific lines
within a source file).
### Preview features
- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### Formatter
- Add `--range` option to `ruff format` ([#9733](https://github.com/astral-sh/ruff/pull/9733))
- Don't trim last empty line in docstrings ([#9813](https://github.com/astral-sh/ruff/pull/9813))
### Bug fixes
- Skip empty lines when determining base indentation ([#9795](https://github.com/astral-sh/ruff/pull/9795))
- Drop `__get__` and `__set__` from `unnecessary-dunder-call` ([#9791](https://github.com/astral-sh/ruff/pull/9791))
- Respect generic `Protocol` in ellipsis removal ([#9841](https://github.com/astral-sh/ruff/pull/9841))
- Revert "Use publicly available Apple Silicon runners (#9726)" ([#9834](https://github.com/astral-sh/ruff/pull/9834))
### Performance
- Skip LibCST parsing for standard dedent adjustments ([#9769](https://github.com/astral-sh/ruff/pull/9769))
- Remove CST-based fixer for `C408` ([#9822](https://github.com/astral-sh/ruff/pull/9822))
- Add our own ignored-names abstractions ([#9802](https://github.com/astral-sh/ruff/pull/9802))
- Remove CST-based fixers for `C400`, `C401`, `C410`, and `C418` ([#9819](https://github.com/astral-sh/ruff/pull/9819))
- Use `AhoCorasick` to speed up quote match ([#9773](https://github.com/astral-sh/ruff/pull/9773))
- Remove CST-based fixers for `C405` and `C409` ([#9821](https://github.com/astral-sh/ruff/pull/9821))
- Add fast-path for comment detection ([#9808](https://github.com/astral-sh/ruff/pull/9808))
- Invert order of checks in `zero-sleep-call` ([#9766](https://github.com/astral-sh/ruff/pull/9766))
- Short-circuit typing matches based on imports ([#9800](https://github.com/astral-sh/ruff/pull/9800))
- Run dunder method rule on methods directly ([#9815](https://github.com/astral-sh/ruff/pull/9815))
- Track top-level module imports in the semantic model ([#9775](https://github.com/astral-sh/ruff/pull/9775))
- Slight speed-up for lowercase and uppercase identifier checks ([#9798](https://github.com/astral-sh/ruff/pull/9798))
- Remove LibCST-based fixer for `C403` ([#9818](https://github.com/astral-sh/ruff/pull/9818))
### Documentation
- Update `max-pos-args` example to `max-positional-args` ([#9797](https://github.com/astral-sh/ruff/pull/9797))
- Fixed example code in `weak_cryptographic_key.rs` ([#9774](https://github.com/astral-sh/ruff/pull/9774))
- Fix references to deprecated `ANN` rules in changelog ([#9771](https://github.com/astral-sh/ruff/pull/9771))
- Fix default for `max-positional-args` ([#9838](https://github.com/astral-sh/ruff/pull/9838))
## 0.2.0
### Breaking changes
@@ -13,8 +58,8 @@ See also, the "Remapped rules" section which may result in disabled rules.
The following rules are now deprecated:
- [`missing-type-function-argument`](https://docs.astral.sh/ruff/rules/missing-type-function-argument/) (`ANN001`)
- [`missing-type-args`](https://docs.astral.sh/ruff/rules/missing-type-args/) (`ANN002`)
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
The following command line options are now deprecated:

View File

@@ -231,7 +231,7 @@ Once you've completed the code for the rule itself, you can define tests with th
For example, if you're adding a new rule named `E402`, you would run:
```shell
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
```
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that

58
Cargo.lock generated
View File

@@ -1090,9 +1090,9 @@ dependencies = [
[[package]]
name = "is-macro"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75828adcb53122ef5ea649a39f50f82d94b754099bf6331b32e255e1891e8fb"
checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f"
dependencies = [
"Inflector",
"proc-macro2",
@@ -1132,9 +1132,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
@@ -1324,9 +1324,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.6.4"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memoffset"
@@ -1780,12 +1780,12 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
checksum = "ef61ae096a2f8c8b49eca360679dbc25f57c99145f6634b6bc18fedb1f9c6c30"
dependencies = [
"indexmap",
"pep440_rs 0.3.12",
"pep440_rs 0.4.0",
"pep508_rs",
"serde",
"toml",
@@ -2005,7 +2005,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"anyhow",
"argfile",
@@ -2023,7 +2023,7 @@ dependencies = [
"insta",
"insta-cmd",
"is-macro",
"itertools 0.12.0",
"itertools 0.12.1",
"log",
"mimalloc",
"notify",
@@ -2081,7 +2081,7 @@ dependencies = [
"filetime",
"glob",
"globset",
"itertools 0.12.0",
"itertools 0.12.1",
"regex",
"ruff_macros",
"seahash",
@@ -2097,7 +2097,7 @@ dependencies = [
"imara-diff",
"indicatif",
"indoc",
"itertools 0.12.0",
"itertools 0.12.1",
"libcst",
"once_cell",
"pretty_assertions",
@@ -2165,7 +2165,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2181,7 +2181,7 @@ dependencies = [
"insta",
"is-macro",
"is-wsl",
"itertools 0.12.0",
"itertools 0.12.1",
"libcst",
"log",
"memchr",
@@ -2233,7 +2233,7 @@ dependencies = [
name = "ruff_macros"
version = "0.0.0"
dependencies = [
"itertools 0.12.0",
"itertools 0.12.1",
"proc-macro2",
"quote",
"ruff_python_trivia",
@@ -2246,7 +2246,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"insta",
"itertools 0.12.0",
"itertools 0.12.1",
"once_cell",
"rand",
"ruff_diagnostics",
@@ -2264,10 +2264,12 @@ dependencies = [
name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.4.1",
"insta",
"is-macro",
"itertools 0.12.0",
"itertools 0.12.1",
"once_cell",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
@@ -2298,7 +2300,7 @@ dependencies = [
"clap",
"countme",
"insta",
"itertools 0.12.0",
"itertools 0.12.1",
"memchr",
"once_cell",
"regex",
@@ -2341,7 +2343,7 @@ dependencies = [
"bitflags 2.4.1",
"hexf-parse",
"is-macro",
"itertools 0.12.0",
"itertools 0.12.1",
"lexical-parse-float",
"rand",
"unic-ucd-category",
@@ -2355,7 +2357,7 @@ dependencies = [
"bitflags 2.4.1",
"insta",
"is-macro",
"itertools 0.12.0",
"itertools 0.12.1",
"lalrpop",
"lalrpop-util",
"memchr",
@@ -2406,7 +2408,7 @@ name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"insta",
"itertools 0.12.0",
"itertools 0.12.1",
"ruff_python_ast",
"ruff_python_index",
"ruff_python_parser",
@@ -2417,7 +2419,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"anyhow",
"clap",
@@ -2488,7 +2490,7 @@ dependencies = [
"globset",
"ignore",
"is-macro",
"itertools 0.12.0",
"itertools 0.12.1",
"log",
"once_cell",
"path-absolutize",
@@ -3058,9 +3060,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
dependencies = [
"serde",
"serde_spanned",
@@ -3079,9 +3081,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"serde",

View File

@@ -47,15 +47,15 @@ indicatif ={ version = "0.17.7"}
indoc ={ version = "2.0.4"}
insta = { version = "1.34.0", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.4" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.12.0" }
itertools = { version = "0.12.1" }
js-sys = { version = "0.3.67" }
lalrpop-util = { version = "0.20.0", default-features = false }
lexical-parse-float = { version = "0.8.0", features = ["format"] }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
memchr = { version = "2.6.4" }
memchr = { version = "2.7.1" }
mimalloc = { version ="0.1.39"}
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
@@ -65,7 +65,7 @@ pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.78" }
pyproject-toml = { version = "0.8.1" }
pyproject-toml = { version = "0.8.2" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
@@ -93,7 +93,7 @@ tempfile = { version ="3.9.0"}
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
tikv-jemallocator = { version ="0.5.0"}
toml = { version = "0.8.8" }
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.2.0
rev: v0.2.1
hooks:
# Run the linter.
- id: ruff
@@ -433,6 +433,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [pytest](https://github.com/pytest-dev/pytest)
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
@@ -463,7 +464,7 @@ Ruff is used by a number of major open-source projects and companies, including:
### Show Your Support
If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
If you're using Ruff, consider adding the Ruff badge to your project's `README.md`:
```md
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -489,6 +490,6 @@ MIT
<div align="center">
<a target="_blank" href="https://astral.sh" style="background:none">
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg">
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg" alt="Made by Astral">
</a>
</div>

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.2.0"
version = "0.2.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,10 @@
use std::cmp::Ordering;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::str::FromStr;
use clap::{command, Parser};
use colored::Colorize;
use regex::Regex;
use rustc_hash::FxHashMap;
@@ -12,6 +16,8 @@ use ruff_linter::settings::types::{
SerializationFormat, UnsafeFixes,
};
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::PycodestyleOptions;
use ruff_workspace::resolver::ConfigurationTransformer;
@@ -440,6 +446,21 @@ pub struct FormatCommand {
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// When specified, Ruff will try to only format the code in the given range.
/// It might be necessary to extend the start backwards or the end forwards, to fully enclose a logical line.
/// The `<RANGE>` uses the format `<start_line>:<start_column>-<end_line>:<end_column>`.
///
/// - The line and column numbers are 1 based.
/// - The column specifies the nth-unicode codepoint on that line.
/// - The end offset is exclusive.
/// - The column numbers are optional. You can write `--range=1-2` instead of `--range=1:1-2:1`.
/// - The end position is optional. You can write `--range=2` to format the entire document starting from the second line.
/// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
///
/// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
#[clap(long, help_heading = "Editor options", verbatim_doc_comment)]
pub range: Option<FormatRange>,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
@@ -570,6 +591,7 @@ impl FormatCommand {
isolated: self.isolated,
no_cache: self.no_cache,
stdin_filename: self.stdin_filename,
range: self.range,
},
CliOverrides {
line_length: self.line_length,
@@ -670,6 +692,196 @@ pub struct FormatArguments {
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
pub range: Option<FormatRange>,
}
/// A text range specified by line and column numbers.
#[derive(Copy, Clone, Debug)]
pub struct FormatRange {
start: LineColumn,
end: LineColumn,
}
impl FormatRange {
/// Converts the line:column range to a byte offset range specific for `source`.
///
/// Returns an empty range if the start range is past the end of `source`.
pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange {
let start_byte_offset = line_index.offset(self.start.line, self.start.column, source);
let end_byte_offset = line_index.offset(self.end.line, self.end.column, source);
TextRange::new(start_byte_offset, end_byte_offset)
}
}
impl FromStr for FormatRange {
type Err = FormatRangeParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (start, end) = value.split_once('-').unwrap_or((value, ""));
let start = if start.is_empty() {
LineColumn::default()
} else {
start.parse().map_err(FormatRangeParseError::InvalidStart)?
};
let end = if end.is_empty() {
LineColumn {
line: OneIndexed::MAX,
column: OneIndexed::MAX,
}
} else {
end.parse().map_err(FormatRangeParseError::InvalidEnd)?
};
if start > end {
return Err(FormatRangeParseError::StartGreaterThanEnd(start, end));
}
Ok(FormatRange { start, end })
}
}
#[derive(Clone, Debug)]
pub enum FormatRangeParseError {
InvalidStart(LineColumnParseError),
InvalidEnd(LineColumnParseError),
StartGreaterThanEnd(LineColumn, LineColumn),
}
impl std::fmt::Display for FormatRangeParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let tip = " tip:".bold().green();
match self {
FormatRangeParseError::StartGreaterThanEnd(start, end) => {
write!(
f,
"the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'",
start_invalid=start.to_string().bold().yellow(),
end_invalid=end.to_string().bold().yellow(),
start=start.to_string().green().bold(),
end=end.to_string().green().bold()
)
}
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
FormatRangeParseError::InvalidEnd(inner) => inner.write(f, false),
}
}
}
impl std::error::Error for FormatRangeParseError {}
#[derive(Copy, Clone, Debug)]
pub struct LineColumn {
pub line: OneIndexed,
pub column: OneIndexed,
}
impl std::fmt::Display for LineColumn {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{line}:{column}", line = self.line, column = self.column)
}
}
impl Default for LineColumn {
fn default() -> Self {
LineColumn {
line: OneIndexed::MIN,
column: OneIndexed::MIN,
}
}
}
impl PartialOrd for LineColumn {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LineColumn {
fn cmp(&self, other: &Self) -> Ordering {
self.line
.cmp(&other.line)
.then(self.column.cmp(&other.column))
}
}
impl PartialEq for LineColumn {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for LineColumn {}
impl FromStr for LineColumn {
type Err = LineColumnParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (line, column) = value.split_once(':').unwrap_or((value, "1"));
let line: usize = line.parse().map_err(LineColumnParseError::LineParseError)?;
let column: usize = column
.parse()
.map_err(LineColumnParseError::ColumnParseError)?;
match (OneIndexed::new(line), OneIndexed::new(column)) {
(Some(line), Some(column)) => Ok(LineColumn { line, column }),
(Some(line), None) => Err(LineColumnParseError::ZeroColumnIndex { line }),
(None, Some(column)) => Err(LineColumnParseError::ZeroLineIndex { column }),
(None, None) => Err(LineColumnParseError::ZeroLineAndColumnIndex),
}
}
}
#[derive(Clone, Debug)]
pub enum LineColumnParseError {
ZeroLineIndex { column: OneIndexed },
ZeroColumnIndex { line: OneIndexed },
ZeroLineAndColumnIndex,
LineParseError(std::num::ParseIntError),
ColumnParseError(std::num::ParseIntError),
}
impl LineColumnParseError {
fn write(&self, f: &mut std::fmt::Formatter, start_range: bool) -> std::fmt::Result {
let tip = "tip:".bold().green();
let range = if start_range { "start" } else { "end" };
match self {
LineColumnParseError::ColumnParseError(inner) => {
write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.")
}
LineColumnParseError::LineParseError(inner) => {
write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.")
}
LineColumnParseError::ZeroColumnIndex { line } => {
write!(
f,
"the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion=format!("{line}:1").green().bold()
)
}
LineColumnParseError::ZeroLineIndex { column } => {
write!(
f,
"the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion=format!("1:{column}").green().bold()
)
}
LineColumnParseError::ZeroLineAndColumnIndex => {
write!(
f,
"the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion="1:1".to_string().green().bold()
)
}
}
}
}
/// CLI settings that function as configuration overrides.

View File

@@ -1050,6 +1050,7 @@ mod tests {
&self.settings.formatter,
PySourceType::Python,
FormatMode::Write,
None,
Some(cache),
)
}

View File

@@ -23,12 +23,13 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle};
use ruff_source_file::LineIndex;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::args::{CliOverrides, FormatArguments, FormatRange};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve;
@@ -77,6 +78,13 @@ pub(crate) fn format(
return Ok(ExitStatus::Success);
}
if cli.range.is_some() && paths.len() > 1 {
return Err(anyhow::anyhow!(
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
paths.len()
));
}
warn_incompatible_formatter_settings(&resolver);
// Discover the package root for each Python file.
@@ -139,7 +147,14 @@ pub(crate) fn format(
Some(
match catch_unwind(|| {
format_path(path, &settings.formatter, source_type, mode, cache)
format_path(
path,
&settings.formatter,
source_type,
mode,
cli.range,
cache,
)
}) {
Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.path().to_path_buf(),
@@ -226,6 +241,7 @@ pub(crate) fn format_path(
settings: &FormatterSettings,
source_type: PySourceType,
mode: FormatMode,
range: Option<FormatRange>,
cache: Option<&Cache>,
) -> Result<FormatResult, FormatCommandError> {
if let Some(cache) = cache {
@@ -250,8 +266,12 @@ pub(crate) fn format_path(
}
};
// Don't write back to the cache if formatting a range.
let cache = cache.filter(|_| range.is_none());
// Format the source.
let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)?
{
FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => {
let mut writer = File::create(path).map_err(|err| {
@@ -319,12 +339,31 @@ pub(crate) fn format_source(
source_type: PySourceType,
path: Option<&Path>,
settings: &FormatterSettings,
range: Option<FormatRange>,
) -> Result<FormattedSource, FormatCommandError> {
match &source_kind {
SourceKind::Python(unformatted) => {
let options = settings.to_format_options(source_type, unformatted);
let formatted = format_module_source(unformatted, options).map_err(|err| {
let formatted = if let Some(range) = range {
let line_index = LineIndex::from_source_text(unformatted);
let byte_range = range.to_text_range(unformatted, &line_index);
format_range(unformatted, byte_range, options).map(|formatted_range| {
let mut formatted = unformatted.to_string();
formatted.replace_range(
std::ops::Range::<usize>::from(formatted_range.source_range()),
formatted_range.as_code(),
);
formatted
})
} else {
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
#[allow(clippy::redundant_closure_for_method_calls)]
format_module_source(unformatted, options).map(|formatted| formatted.into_code())
};
let formatted = formatted.map_err(|err| {
if let FormatModuleError::ParseError(err) = err {
DisplayParseError::from_source_kind(
err,
@@ -337,7 +376,6 @@ pub(crate) fn format_source(
}
})?;
let formatted = formatted.into_code();
if formatted.len() == unformatted.len() && formatted == *unformatted {
Ok(FormattedSource::Unchanged)
} else {
@@ -349,6 +387,12 @@ pub(crate) fn format_source(
return Ok(FormattedSource::Unchanged);
}
if range.is_some() {
return Err(FormatCommandError::RangeFormatNotebook(
path.map(Path::to_path_buf),
));
}
let options = settings.to_format_options(source_type, notebook.source_code());
let mut output: Option<String> = None;
@@ -589,6 +633,7 @@ pub(crate) enum FormatCommandError {
Format(Option<PathBuf>, FormatModuleError),
Write(Option<PathBuf>, SourceError),
Diff(Option<PathBuf>, io::Error),
RangeFormatNotebook(Option<PathBuf>),
}
impl FormatCommandError {
@@ -606,7 +651,8 @@ impl FormatCommandError {
| Self::Read(path, _)
| Self::Format(path, _)
| Self::Write(path, _)
| Self::Diff(path, _) => path.as_deref(),
| Self::Diff(path, _)
| Self::RangeFormatNotebook(path) => path.as_deref(),
}
}
}
@@ -628,9 +674,10 @@ impl Display for FormatCommandError {
} else {
write!(
f,
"{} {}",
"Encountered error:".bold(),
err.io_error()
"{header} {error}",
header = "Encountered error:".bold(),
error = err
.io_error()
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
)
}
@@ -648,7 +695,7 @@ impl Display for FormatCommandError {
":".bold()
)
} else {
write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
write!(f, "{header} {err}", header = "Failed to read:".bold())
}
}
Self::Write(path, err) => {
@@ -661,7 +708,7 @@ impl Display for FormatCommandError {
":".bold()
)
} else {
write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
write!(f, "{header} {err}", header = "Failed to write:".bold())
}
}
Self::Format(path, err) => {
@@ -674,7 +721,7 @@ impl Display for FormatCommandError {
":".bold()
)
} else {
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
write!(f, "{header} {err}", header = "Failed to format:".bold())
}
}
Self::Diff(path, err) => {
@@ -689,9 +736,25 @@ impl Display for FormatCommandError {
} else {
write!(
f,
"{}{} {err}",
"Failed to generate diff".bold(),
":".bold()
"{header} {err}",
header = "Failed to generate diff:".bold(),
)
}
}
Self::RangeFormatNotebook(path) => {
if let Some(path) = path {
write!(
f,
"{header}{path}{colon} Range formatting isn't supported for notebooks.",
header = "Failed to format ".bold(),
path = fs::relativize_path(path).bold(),
colon = ":".bold()
)
} else {
write!(
f,
"{header} Range formatting isn't supported for notebooks",
header = "Failed to format:".bold()
)
}
}

View File

@@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::args::{CliOverrides, FormatArguments, FormatRange};
use crate::commands::format::{
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
FormatResult, FormattedSource,
@@ -69,7 +69,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
};
// Format the file.
match format_source_code(path, settings, source_type, mode) {
match format_source_code(path, cli.range, settings, source_type, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {
@@ -90,6 +90,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
/// Format source code read from `stdin`.
fn format_source_code(
path: Option<&Path>,
range: Option<FormatRange>,
settings: &FormatterSettings,
source_type: PySourceType,
mode: FormatMode,
@@ -107,7 +108,7 @@ fn format_source_code(
};
// Format the source.
let formatted = format_source(&source_kind, source_type, path, settings)?;
let formatted = format_source(&source_kind, source_type, path, settings, range)?;
match &formatted {
FormattedSource::Formatted(formatted) => match mode {

View File

@@ -1544,3 +1544,322 @@ include = ["*.ipy"]
"###);
Ok(())
}
#[test]
fn range_formatting() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:8-2:14"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Shouldn't format this" )
----- stderr -----
"###);
}
#[test]
fn range_formatting_unicode() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:21-3"])
.arg("-")
.pass_stdin(r#"
def foo(arg1="👋🏽" ): print("Format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1="👋🏽" ):
print("Format this")
----- stderr -----
"###);
}
#[test]
fn range_formatting_multiple_files() -> std::io::Result<()> {
let tempdir = TempDir::new()?;
let file1 = tempdir.path().join("file1.py");
fs::write(
&file1,
r#"
def file1(arg1, arg2,):
print("Shouldn't format this" )
"#,
)?;
let file2 = tempdir.path().join("file2.py");
fs::write(
&file2,
r#"
def file2(arg1, arg2,):
print("Shouldn't format this" )
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--range=1:8-1:15"])
.arg(file1)
.arg(file2), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The `--range` option is only supported when formatting a single file but the specified paths resolve to 2 files.
"###);
Ok(())
}
#[test]
fn range_formatting_out_of_bounds() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=100:40-200:1"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1, arg2,):
print("Shouldn't format this" )
----- stderr -----
"###);
}
#[test]
fn range_start_larger_than_end() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=90-50"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't format this" )
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value '90-50' for '--range <RANGE>': the start position '90:1' is greater than the end position '50:1'.
tip: Try switching start and end: '50:1-90:1'
For more information, try '--help'.
"###);
}
#[test]
fn range_line_numbers_only() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2-3"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Shouldn't format this" )
----- stderr -----
"###);
}
#[test]
fn range_start_only() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=3"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Should format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1, arg2,):
print("Should format this")
----- stderr -----
"###);
}
#[test]
fn range_end_only() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=-3"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Should format this" )
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Should format this" )
----- stderr -----
"###);
}
#[test]
fn range_missing_line() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=1-:20"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Should format this" )
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value '1-:20' for '--range <RANGE>': the end line is not a valid number (cannot parse integer from empty string)
tip: The format is 'line:column'.
For more information, try '--help'.
"###);
}
#[test]
fn zero_line_number() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:2"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Should format this" )
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value '0:2' for '--range <RANGE>': the start line is 0, but it should be 1 or greater.
tip: The line numbers start at 1.
tip: Try 1:2 instead.
For more information, try '--help'.
"###);
}
#[test]
fn column_and_line_zero() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:0"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Should format this" )
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value '0:0' for '--range <RANGE>': the start line and column are both 0, but they should be 1 or greater.
tip: The line and column numbers start at 1.
tip: Try 1:1 instead.
For more information, try '--help'.
"###);
}
#[test]
fn range_formatting_notebook() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--no-cache", "--stdin-filename", "main.ipynb", "--range=1-2"])
.arg("-")
.pass_stdin(r#"
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
"metadata": {},
"outputs": [],
"source": [
"x=1"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
"###);
}

View File

@@ -308,11 +308,8 @@ impl std::fmt::Debug for Token {
/// assert_eq!(printed.as_code(), r#""Hello 'Ruff'""#);
/// assert_eq!(printed.sourcemap(), [
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(0) },
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(7) },
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(7) },
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(13) },
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(13) },
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(14) },
/// SourceMarker { source: TextSize::new(20), dest: TextSize::new(14) },
/// ]);
///
@@ -340,18 +337,18 @@ impl<Context> Format<Context> for SourcePosition {
}
}
/// Creates a text from a dynamic string with its optional start-position in the source document.
/// Creates a text from a dynamic string.
///
/// This is done by allocating a new string internally.
pub fn text(text: &str, position: Option<TextSize>) -> Text {
pub fn text(text: &str) -> Text {
debug_assert_no_newlines(text);
Text { text, position }
Text { text }
}
#[derive(Eq, PartialEq)]
pub struct Text<'a> {
text: &'a str,
position: Option<TextSize>,
}
impl<Context> Format<Context> for Text<'_>
@@ -359,10 +356,6 @@ where
Context: FormatContext,
{
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
if let Some(position) = self.position {
source_position(position).fmt(f)?;
}
f.write_element(FormatElement::Text {
text: self.text.to_string().into_boxed_str(),
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
@@ -2292,7 +2285,7 @@ impl<Context, T> std::fmt::Debug for FormatWith<Context, T> {
/// let mut join = f.join_with(&separator);
///
/// for item in &self.items {
/// join.entry(&format_with(|f| write!(f, [text(item, None)])));
/// join.entry(&format_with(|f| write!(f, [text(item)])));
/// }
/// join.finish()
/// })),
@@ -2377,7 +2370,7 @@ where
/// let mut count = 0;
///
/// let value = format_once(|f| {
/// write!(f, [text(&std::format!("Formatted {count}."), None)])
/// write!(f, [text(&std::format!("Formatted {count}."))])
/// });
///
/// format!(SimpleFormatContext::default(), [value]).expect("Formatting once works fine");

View File

@@ -346,10 +346,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
}
FormatElement::SourcePosition(position) => {
write!(
f,
[text(&std::format!("source_position({position:?})"), None)]
)?;
write!(f, [text(&std::format!("source_position({position:?})"))])?;
}
FormatElement::LineSuffixBoundary => {
@@ -360,7 +357,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(f, [token("best_fitting(")])?;
if *mode != BestFittingMode::FirstLine {
write!(f, [text(&std::format!("mode: {mode:?}, "), None)])?;
write!(f, [text(&std::format!("mode: {mode:?}, "))])?;
}
write!(f, [token("[")])?;
@@ -392,17 +389,14 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(
f,
[
text(&std::format!("<interned {index}>"), None),
text(&std::format!("<interned {index}>")),
space(),
&&**interned,
]
)?;
}
Some(reference) => {
write!(
f,
[text(&std::format!("<ref interned *{reference}>"), None)]
)?;
write!(f, [text(&std::format!("<ref interned *{reference}>"))])?;
}
}
}
@@ -421,7 +415,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f,
[
token("<END_TAG_WITHOUT_START<"),
text(&std::format!("{:?}", tag.kind()), None),
text(&std::format!("{:?}", tag.kind())),
token(">>"),
]
)?;
@@ -436,9 +430,9 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
token(")"),
soft_line_break_or_space(),
token("ERROR<START_END_TAG_MISMATCH<start: "),
text(&std::format!("{start_kind:?}"), None),
text(&std::format!("{start_kind:?}")),
token(", end: "),
text(&std::format!("{:?}", tag.kind()), None),
text(&std::format!("{:?}", tag.kind())),
token(">>")
]
)?;
@@ -470,7 +464,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f,
[
token("align("),
text(&count.to_string(), None),
text(&count.to_string()),
token(","),
space(),
]
@@ -482,7 +476,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f,
[
token("line_suffix("),
text(&std::format!("{reserved_width:?}"), None),
text(&std::format!("{reserved_width:?}")),
token(","),
space(),
]
@@ -499,11 +493,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = group.id() {
write!(
f,
[
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
)?;
}
@@ -524,11 +514,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = id {
write!(
f,
[
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
)?;
}
}
@@ -561,7 +547,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f,
[
token("indent_if_group_breaks("),
text(&std::format!("\"{id:?}\""), None),
text(&std::format!("\"{id:?}\"")),
token(","),
space(),
]
@@ -581,11 +567,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
if let Some(group_id) = condition.group_id {
write!(
f,
[
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
[text(&std::format!("\"{group_id:?}\"")), token(","), space()]
)?;
}
}
@@ -595,7 +577,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
f,
[
token("label("),
text(&std::format!("\"{label_id:?}\""), None),
text(&std::format!("\"{label_id:?}\"")),
token(","),
space(),
]
@@ -664,7 +646,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
ContentArrayEnd,
token(")"),
soft_line_break_or_space(),
text(&std::format!("<START_WITHOUT_END<{top:?}>>"), None),
text(&std::format!("<START_WITHOUT_END<{top:?}>>")),
]
)?;
}
@@ -807,7 +789,7 @@ impl Format<IrFormatContext<'_>> for Condition {
f,
[
token("if_group_fits_on_line("),
text(&std::format!("\"{id:?}\""), None),
text(&std::format!("\"{id:?}\"")),
token(")")
]
),
@@ -816,7 +798,7 @@ impl Format<IrFormatContext<'_>> for Condition {
f,
[
token("if_group_breaks("),
text(&std::format!("\"{id:?}\""), None),
text(&std::format!("\"{id:?}\"")),
token(")")
]
),

View File

@@ -32,7 +32,7 @@ pub trait MemoizeFormat<Context> {
/// let value = self.value.get();
/// self.value.set(value + 1);
///
/// write!(f, [text(&std::format!("Formatted {value} times."), None)])
/// write!(f, [text(&std::format!("Formatted {value} times."))])
/// }
/// }
///
@@ -110,7 +110,7 @@ where
/// write!(f, [
/// token("Count:"),
/// space(),
/// text(&std::format!("{current}"), None),
/// text(&std::format!("{current}")),
/// hard_line_break()
/// ])?;
///

View File

@@ -41,7 +41,7 @@ use std::marker::PhantomData;
use std::num::{NonZeroU16, NonZeroU8, TryFromIntError};
use crate::format_element::document::Document;
use crate::printer::{Printer, PrinterOptions, SourceMapGeneration};
use crate::printer::{Printer, PrinterOptions};
pub use arguments::{Argument, Arguments};
pub use buffer::{
Buffer, BufferExtensions, BufferSnapshot, Inspect, RemoveSoftLinesBuffer, VecBuffer,
@@ -269,7 +269,6 @@ impl FormatOptions for SimpleFormatOptions {
line_width: self.line_width,
indent_style: self.indent_style,
indent_width: self.indent_width,
source_map_generation: SourceMapGeneration::Enabled,
..PrinterOptions::default()
}
}
@@ -433,28 +432,40 @@ impl Printed {
std::mem::take(&mut self.verbatim_ranges)
}
/// Slices the formatted code to the sub-slices that covers the passed `source_range`.
/// Slices the formatted code to the sub-slices that covers the passed `source_range` in `source`.
///
/// The implementation uses the source map generated during formatting to find the closest range
/// in the formatted document that covers `source_range` or more. The returned slice
/// matches the `source_range` exactly (except indent, see below) if the formatter emits [`FormatElement::SourcePosition`] for
/// the range's offsets.
///
/// ## Indentation
/// The indentation before `source_range.start` is replaced with the indentation returned by the formatter
/// to fix up incorrectly intended code.
///
/// Returns the entire document if the source map is empty.
///
/// # Panics
/// If `source_range` points to offsets that are not in the bounds of `source`.
#[must_use]
pub fn slice_range(self, source_range: TextRange) -> PrintedRange {
pub fn slice_range(self, source_range: TextRange, source: &str) -> PrintedRange {
let mut start_marker: Option<SourceMarker> = None;
let mut end_marker: Option<SourceMarker> = None;
// Note: The printer can generate multiple source map entries for the same source position.
// For example if you have:
// * token("a + b")
// * `source_position(276)`
// * `token("def")`
// * `token("foo")`
// * `source_position(284)`
// The printer uses the source position 276 for both the tokens `def` and `foo` because that's the only position it knows of.
// * `token(")")`
// * `source_position(276)`
// * `hard_line_break`
// The printer uses the source position 276 for both the tokens `)` and the `\n` because
// there were multiple `source_position` entries in the IR with the same offset.
// This can happen if multiple nodes start or end at the same position. A common example
// for this are expressions and expression statement that always end at the same offset.
//
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed.
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed
// and depends on the emitted `IR`.
// They are only guaranteed to be sorted in increasing order by their destination position.
for marker in self.sourcemap {
// Take the closest start marker, but skip over start_markers that have the same start.
@@ -471,17 +482,44 @@ impl Printed {
}
}
let start = start_marker.map(|marker| marker.dest).unwrap_or_default();
let end = end_marker.map_or_else(|| self.code.text_len(), |marker| marker.dest);
let code_range = TextRange::new(start, end);
let (source_start, formatted_start) = start_marker
.map(|marker| (marker.source, marker.dest))
.unwrap_or_default();
let (source_end, formatted_end) = end_marker
.map_or((source.text_len(), self.code.text_len()), |marker| {
(marker.source, marker.dest)
});
let source_range = TextRange::new(source_start, source_end);
let formatted_range = TextRange::new(formatted_start, formatted_end);
// Extend both ranges to include the indentation
let source_range = extend_range_to_include_indent(source_range, source);
let formatted_range = extend_range_to_include_indent(formatted_range, &self.code);
PrintedRange {
code: self.code[code_range].to_string(),
code: self.code[formatted_range].to_string(),
source_range,
}
}
}
/// Extends `range` backwards (by reducing `range.start`) to include any directly preceding whitespace (`\t` or ` `).
///
/// # Panics
/// If `range.start` is out of `source`'s bounds.
fn extend_range_to_include_indent(range: TextRange, source: &str) -> TextRange {
let whitespace_len: TextSize = source[..usize::from(range.start())]
.chars()
.rev()
.take_while(|c| matches!(c, ' ' | '\t'))
.map(TextLen::text_len)
.sum();
TextRange::new(range.start() - whitespace_len, range.end())
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -537,7 +575,7 @@ pub type FormatResult<F> = Result<F, FormatError>;
/// impl Format<SimpleFormatContext> for Paragraph {
/// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
/// write!(f, [
/// text(&self.0, None),
/// text(&self.0),
/// hard_line_break(),
/// ])
/// }

View File

@@ -4,7 +4,7 @@ use drop_bomb::DebugDropBomb;
use unicode_width::UnicodeWidthChar;
pub use printer_options::*;
use ruff_text_size::{Ranged, TextLen, TextSize};
use ruff_text_size::{TextLen, TextSize};
use crate::format_element::document::Document;
use crate::format_element::tag::{Condition, GroupMode};
@@ -76,6 +76,9 @@ impl<'a> Printer<'a> {
}
}
// Push any pending marker
self.push_marker();
Ok(Printed::new(
self.state.buffer,
None,
@@ -97,42 +100,38 @@ impl<'a> Printer<'a> {
let args = stack.top();
match element {
FormatElement::Space => self.print_text(Text::Token(" "), None),
FormatElement::Token { text } => self.print_text(Text::Token(text), None),
FormatElement::Text { text, text_width } => self.print_text(
Text::Text {
text,
text_width: *text_width,
},
None,
),
FormatElement::Space => self.print_text(Text::Token(" ")),
FormatElement::Token { text } => self.print_text(Text::Token(text)),
FormatElement::Text { text, text_width } => self.print_text(Text::Text {
text,
text_width: *text_width,
}),
FormatElement::SourceCodeSlice { slice, text_width } => {
let text = slice.text(self.source_code);
self.print_text(
Text::Text {
text,
text_width: *text_width,
},
Some(slice.range()),
);
self.print_text(Text::Text {
text,
text_width: *text_width,
});
}
FormatElement::Line(line_mode) => {
if args.mode().is_flat()
&& matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace)
{
if line_mode == &LineMode::SoftOrSpace {
self.print_text(Text::Token(" "), None);
self.print_text(Text::Token(" "));
}
} else if self.state.line_suffixes.has_pending() {
self.flush_line_suffixes(queue, stack, Some(element));
} else {
// Only print a newline if the current line isn't already empty
if self.state.line_width > 0 {
self.push_marker();
self.print_char('\n');
}
// Print a second line break if this is an empty line
if line_mode == &LineMode::Empty {
self.push_marker();
self.print_char('\n');
}
@@ -145,14 +144,11 @@ impl<'a> Printer<'a> {
}
FormatElement::SourcePosition(position) => {
self.state.source_position = *position;
// The printer defers printing indents until the next text
// is printed. Pushing the marker now would mean that the
// mapped range includes the indent range, which we don't want.
// Only add a marker if we're not in an indented context, e.g. at the end of the file.
if self.state.pending_indent.is_empty() {
self.push_marker();
}
// Queue the source map position and emit it when printing the next character
self.state.pending_source_position = Some(*position);
}
FormatElement::LineSuffixBoundary => {
@@ -444,7 +440,7 @@ impl<'a> Printer<'a> {
Ok(print_mode)
}
fn print_text(&mut self, text: Text, source_range: Option<TextRange>) {
fn print_text(&mut self, text: Text) {
if !self.state.pending_indent.is_empty() {
let (indent_char, repeat_count) = match self.options.indent_style() {
IndentStyle::Tab => ('\t', 1),
@@ -467,19 +463,6 @@ impl<'a> Printer<'a> {
}
}
// Insert source map markers before and after the token
//
// If the token has source position information the start marker
// will use the start position of the original token, and the end
// marker will use that position + the text length of the token
//
// If the token has no source position (was created by the formatter)
// both the start and end marker will use the last known position
// in the input source (from state.source_position)
if let Some(range) = source_range {
self.state.source_position = range.start();
}
self.push_marker();
match text {
@@ -502,21 +485,15 @@ impl<'a> Printer<'a> {
}
}
}
if let Some(range) = source_range {
self.state.source_position = range.end();
}
self.push_marker();
}
fn push_marker(&mut self) {
if self.options.source_map_generation.is_disabled() {
let Some(source_position) = self.state.pending_source_position.take() else {
return;
}
};
let marker = SourceMarker {
source: self.state.source_position,
source: source_position,
dest: self.state.buffer.text_len(),
};
@@ -897,7 +874,7 @@ enum FillPairLayout {
struct PrinterState<'a> {
buffer: String,
source_markers: Vec<SourceMarker>,
source_position: TextSize,
pending_source_position: Option<TextSize>,
pending_indent: Indention,
measured_group_fits: bool,
line_width: u32,
@@ -1752,7 +1729,7 @@ a",
let result = format_with_options(
&format_args![
token("function main() {"),
block_indent(&text("let x = `This is a multiline\nstring`;", None)),
block_indent(&text("let x = `This is a multiline\nstring`;")),
token("}"),
hard_line_break()
],
@@ -1769,7 +1746,7 @@ a",
fn it_breaks_a_group_if_a_string_contains_a_newline() {
let result = format(&FormatArrayElements {
items: vec![
&text("`This is a string spanning\ntwo lines`", None),
&text("`This is a string spanning\ntwo lines`"),
&token("\"b\""),
],
});

View File

@@ -14,10 +14,6 @@ pub struct PrinterOptions {
/// The type of line ending to apply to the printed input
pub line_ending: LineEnding,
/// Whether the printer should build a source map that allows mapping positions in the source document
/// to positions in the formatted document.
pub source_map_generation: SourceMapGeneration,
}
impl<'a, O> From<&'a O> for PrinterOptions

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.2.0"
version = "0.2.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -13,3 +13,11 @@ s = f"{set([f(x) for x in 'ab'])}"
s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
s = set( # comment
[x for x in range(3)]
)
s = set([ # comment
x for x in range(3)
])

View File

@@ -20,3 +20,10 @@ f"{dict(x='y') | dict(y='z')}"
f"{ dict(x='y') | dict(y='z') }"
f"a {dict(x='y') | dict(y='z')} b"
f"a { dict(x='y') | dict(y='z') } b"
dict(
# comment
)
tuple( # comment
)

View File

@@ -8,3 +8,11 @@ t4 = tuple([
t5 = tuple(
(1, 2)
)
tuple( # comment
[1, 2]
)
tuple([ # comment
1, 2
])

View File

@@ -2,3 +2,12 @@ l1 = list([1, 2])
l2 = list((1, 2))
l3 = list([])
l4 = list(())
list( # comment
[1, 2]
)
list([ # comment
1, 2
])

View File

@@ -207,3 +207,23 @@ class Repro:
def stub(self) -> str:
"""Docstring"""
...
class Repro(Protocol[int]):
def func(self) -> str:
"""Docstring"""
...
def impl(self) -> str:
"""Docstring"""
return self.func()
class Repro[int](Protocol):
def func(self) -> str:
"""Docstring"""
...
def impl(self) -> str:
"""Docstring"""
return self.func()

View File

@@ -198,3 +198,43 @@ else:
def sb(self):
if self._sb is not None: return self._sb
else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
def indent(x, y, w, z):
if x: # [no-else-return]
a = 1
return y
else:
c = 3
return z
def indent(x, y, w, z):
if x: # [no-else-return]
a = 1
return y
else:
# comment
c = 3
return z
def indent(x, y, w, z):
if x: # [no-else-return]
a = 1
return y
else:
# comment
c = 3
return z
def indent(x, y, w, z):
if x: # [no-else-return]
a = 1
return y
else:
# comment
c = 3
return z

View File

@@ -1,2 +1,15 @@
'''trailing whitespace
inside a multiline string'''
f'''trailing whitespace
inside a multiline f-string'''
# Trailing whitespace after `{`
f'abc {
1 + 2
}'
# Trailing whitespace after `2`
f'abc {
1 + 2
}'

View File

@@ -30,6 +30,11 @@ class Thing:
def do_thing(self, item):
return object.__getattribute__(self, item) # PLC2801
def use_descriptor(self, item):
item.__get__(self, type(self)) # OK
item.__set__(self, 1) # OK
item.__delete__(self) # OK
blah = lambda: {"a": 1}.__delitem__("a") # OK

View File

@@ -215,3 +215,13 @@ if sys.version_info[:2] > (3,13):
if sys.version_info[:3] > (3,13):
print("py3")
if sys.version_info > (3,0):
f"this is\
allowed too"
f"""the indentation on
this line is significant"""
"this is\
allowed too"

View File

@@ -0,0 +1,85 @@
val = 2
def simple_cases():
a = 4
b = "{a}" # RUF027
c = "{a} {b} f'{val}' " # RUF027
def escaped_string():
a = 4
b = "escaped string: {{ brackets surround me }}" # RUF027
def raw_string():
a = 4
b = r"raw string with formatting: {a}" # RUF027
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
def print_name(name: str):
a = 4
print("Hello, {name}!") # RUF027
print("The test value we're using today is {a}") # RUF027
def do_nothing(a):
return a
def nested_funcs():
a = 4
print(do_nothing(do_nothing("{a}"))) # RUF027
def tripled_quoted():
a = 4
c = a
single_line = """ {a} """ # RUF027
# RUF027
multi_line = a = """b { # comment
c} d
"""
def single_quoted_multi_line():
a = 4
# RUF027
b = " {\
a} \
"
def implicit_concat():
a = 4
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
print(f"{a}" "{a}" f"{b}") # RUF027
def escaped_chars():
a = 4
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
def alternative_formatter(src, **kwargs):
src.format(**kwargs)
def format2(src, *args):
pass
# These should not cause an RUF027 message
def negative_cases():
a = 4
positive = False
"""{a}"""
"don't format: {a}"
c = """ {b} """
d = "bad variable: {invalid}"
e = "incorrect syntax: {}"
json = "{ positive: false }"
json2 = "{ 'positive': false }"
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a = 5)
formatted = "{a}".fmt(a = 7)
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a = 5)))
print(format(do_nothing("{a}"), a = 5))
print("{a}".to_upper())
print(do_nothing("{a}").format(a = "Test"))
print(do_nothing("{a}").format2(a))
a = 4
"always ignore this: {a}"
print("but don't ignore this: {val}") # RUF027

View File

@@ -1,13 +1,15 @@
use ruff_python_ast::str::raw_contents_range;
use ruff_text_size::{Ranged, TextRange};
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
use ruff_python_semantic::{
BindingKind, ContextualizedDefinition, Definition, Export, Member, MemberKind,
};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::docstrings::Docstring;
use crate::fs::relativize_path;
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle};
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle, pylint};
use crate::{docstrings, warn_user};
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
@@ -31,6 +33,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
]);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName);
let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection,
Rule::BlankLineAfterSummary,
@@ -80,7 +83,12 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicPackage,
]);
if !enforce_annotations && !enforce_docstrings && !enforce_stubs && !enforce_stubs_and_runtime {
if !enforce_annotations
&& !enforce_docstrings
&& !enforce_stubs
&& !enforce_stubs_and_runtime
&& !enforce_dunder_method
{
return;
}
@@ -147,6 +155,19 @@ pub(crate) fn definitions(checker: &mut Checker) {
}
}
// pylint
if enforce_dunder_method {
if checker.enabled(Rule::BadDunderMethodName) {
if let Definition::Member(Member {
kind: MemberKind::Method(method),
..
}) = definition
{
pylint::rules::bad_dunder_method_name(checker, method);
}
}
}
// pydocstyle
if enforce_docstrings {
if pydocstyle::helpers::should_ignore_definition(

View File

@@ -319,7 +319,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
numpy::rules::numpy_2_0_deprecation(checker, expr);
}
if checker.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_attribute(checker, expr);
pyupgrade::rules::deprecated_mock_attribute(checker, attribute);
}
if checker.enabled(Rule::SixPY3) {
flake8_2020::rules::name_or_attribute(checker, expr);
@@ -336,7 +336,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UndocumentedWarn) {
flake8_logging::rules::undocumented_warn(checker, expr);
}
pandas_vet::rules::attr(checker, attribute);
if checker.enabled(Rule::PandasUseOfDotValues) {
pandas_vet::rules::attr(checker, attribute);
}
}
Expr::Call(
call @ ast::ExprCall {
@@ -636,14 +638,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::tarfile_unsafe_members(checker, call);
}
if checker.enabled(Rule::UnnecessaryGeneratorList) {
flake8_comprehensions::rules::unnecessary_generator_list(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_generator_list(checker, call);
}
if checker.enabled(Rule::UnnecessaryGeneratorSet) {
flake8_comprehensions::rules::unnecessary_generator_set(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_generator_set(checker, call);
}
if checker.enabled(Rule::UnnecessaryGeneratorDict) {
flake8_comprehensions::rules::unnecessary_generator_dict(
@@ -651,9 +649,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::UnnecessaryListComprehensionSet) {
flake8_comprehensions::rules::unnecessary_list_comprehension_set(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_list_comprehension_set(checker, call);
}
if checker.enabled(Rule::UnnecessaryListComprehensionDict) {
flake8_comprehensions::rules::unnecessary_list_comprehension_dict(
@@ -661,9 +657,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::UnnecessaryLiteralSet) {
flake8_comprehensions::rules::unnecessary_literal_set(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_literal_set(checker, call);
}
if checker.enabled(Rule::UnnecessaryLiteralDict) {
flake8_comprehensions::rules::unnecessary_literal_dict(
@@ -673,27 +667,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryCollectionCall) {
flake8_comprehensions::rules::unnecessary_collection_call(
checker,
expr,
func,
args,
keywords,
call,
&checker.settings.flake8_comprehensions,
);
}
if checker.enabled(Rule::UnnecessaryLiteralWithinTupleCall) {
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryLiteralWithinListCall) {
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_literal_within_list_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryLiteralWithinDictCall) {
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(
checker, expr, func, args, keywords,
);
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryListCall) {
flake8_comprehensions::rules::unnecessary_list_call(checker, expr, func, args);
@@ -976,7 +961,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_extend_call(checker, call);
}
if checker.enabled(Rule::DefaultFactoryKwarg) {
ruff::rules::default_factory_kwarg(checker, call);
}
@@ -1041,6 +1025,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::unicode_kind_prefix(checker, string_literal);
}
}
if checker.enabled(Rule::MissingFStringSyntax) {
for string_literal in value.literals() {
ruff::rules::missing_fstring_syntax(
&mut checker.diagnostics,
string_literal,
checker.locator,
&checker.semantic,
);
}
}
}
Expr::BinOp(ast::ExprBinOp {
left,
@@ -1312,12 +1306,22 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::math_constant(checker, number_literal);
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => {
if checker.enabled(Rule::UnicodeKindPrefix) {
for string_part in value {
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
}
}
if checker.enabled(Rule::MissingFStringSyntax) {
for string_literal in value.as_slice() {
ruff::rules::missing_fstring_syntax(
&mut checker.diagnostics,
string_literal,
checker.locator,
&checker.semantic,
);
}
}
}
Expr::IfExp(
if_exp @ ast::ExprIfExp {

View File

@@ -513,9 +513,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(checker, class_def);
}
if checker.enabled(Rule::BadDunderMethodName) {
pylint::rules::bad_dunder_method_name(checker, body);
}
if checker.enabled(Rule::MetaClassABCMeta) {
refurb::rules::metaclass_abcmeta(checker, class_def);
}

View File

@@ -197,7 +197,7 @@ impl<'a> Checker<'a> {
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
// Invert the quote character, if it's a single quote.
match *trailing_quote {
match trailing_quote {
"'" => Some(Quote::Double),
"\"" => Some(Quote::Single),
_ => None,
@@ -349,13 +349,6 @@ where
}
}
// Track each top-level import, to guide import insertions.
if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) {
if self.semantic.at_top_level() {
self.importer.visit_import(stmt);
}
}
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
let flags_snapshot = self.semantic.flags;
@@ -371,14 +364,22 @@ where
self.handle_node_load(target);
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if self.semantic.at_top_level() {
self.importer.visit_import(stmt);
}
for alias in names {
if alias.name.contains('.') && alias.asname.is_none() {
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
// "foo.bar".
let name = alias.name.split('.').next().unwrap();
// Given `import foo.bar`, `module` would be "foo", and `call_path` would be
// `["foo", "bar"]`.
let module = alias.name.split('.').next().unwrap();
// Mark the top-level module as "seen" by the semantic model.
self.semantic.add_module(module);
if alias.asname.is_none() && alias.name.contains('.') {
let call_path: Box<[&str]> = alias.name.split('.').collect();
self.add_binding(
name,
module,
alias.identifier(),
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
BindingFlags::EXTERNAL,
@@ -413,8 +414,20 @@ where
level,
range: _,
}) => {
if self.semantic.at_top_level() {
self.importer.visit_import(stmt);
}
let module = module.as_deref();
let level = *level;
// Mark the top-level module as "seen" by the semantic model.
if level.map_or(true, |level| level == 0) {
if let Some(module) = module.and_then(|module| module.split('.').next()) {
self.semantic.add_module(module);
}
}
for alias in names {
if let Some("__future__") = module {
let name = alias.asname.as_ref().unwrap_or(&alias.name);

View File

@@ -937,6 +937,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(feature = "test-rules")]

View File

@@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::textwrap::dedent_to;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
SimpleTokenizer,
@@ -15,7 +16,9 @@ use ruff_python_trivia::{
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::cst::matchers::{match_function_def, match_indented_block, match_statement};
use crate::fix::codemods;
use crate::fix::codemods::CodegenStylist;
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
/// Return the `Fix` to use when deleting a `Stmt`.
@@ -166,6 +169,50 @@ pub(crate) fn add_argument(
}
}
/// Safely adjust the indentation of the indented block at [`TextRange`].
///
/// The [`TextRange`] is assumed to represent an entire indented block, including the leading
/// indentation of that block. For example, to dedent the body here:
/// ```python
/// if True:
/// print("Hello, world!")
/// ```
///
/// The range would be the entirety of ` print("Hello, world!")`.
pub(crate) fn adjust_indentation(
range: TextRange,
indentation: &str,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<String> {
// If the range includes a multi-line string, use LibCST to ensure that we don't adjust the
// whitespace _within_ the string.
if indexer.multiline_ranges().intersects(range) || indexer.fstring_ranges().intersects(range) {
let contents = locator.slice(range);
let module_text = format!("def f():{}{contents}", stylist.line_ending().as_str());
let mut tree = match_statement(&module_text)?;
let embedding = match_function_def(&mut tree)?;
let indented_block = match_indented_block(&mut embedding.body)?;
indented_block.indent = Some(indentation);
let module_text = indented_block.codegen_stylist(stylist);
let module_text = module_text
.strip_prefix(stylist.line_ending().as_str())
.unwrap()
.to_string();
Ok(module_text)
} else {
// Otherwise, we can do a simple adjustment ourselves.
let contents = locator.slice(range);
Ok(dedent_to(contents, indentation))
}
}
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value

View File

@@ -1,14 +1,16 @@
/// See: [eradicate.py](https://github.com/myint/eradicate/blob/98f199940979c94447a461d50d27862b118b282d/eradicate.py)
use aho_corasick::AhoCorasick;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::{Regex, RegexSet};
use ruff_python_parser::parse_suite;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextSize;
static CODE_INDICATORS: Lazy<AhoCorasick> = Lazy::new(|| {
AhoCorasick::new([
"(", ")", "[", "]", "{", "}", ":", "=", "%", "print", "return", "break", "continue",
"import",
"(", ")", "[", "]", "{", "}", ":", "=", "%", "return", "break", "continue", "import",
])
.unwrap()
});
@@ -44,6 +46,14 @@ pub(crate) fn comment_contains_code(line: &str, task_tags: &[String]) -> bool {
return false;
}
// Fast path: if the comment contains consecutive identifiers, we know it won't parse.
let tokenizer = SimpleTokenizer::starts_at(TextSize::default(), line).skip_trivia();
if tokenizer.tuple_windows().any(|(first, second)| {
first.kind == SimpleTokenKind::Name && second.kind == SimpleTokenKind::Name
}) {
return false;
}
// Ignore task tag comments (e.g., "# TODO(tom): Refactor").
if line
.split(&[' ', ':', '('])
@@ -123,9 +133,10 @@ mod tests {
#[test]
fn comment_contains_code_with_print() {
assert!(comment_contains_code("#print", &[]));
assert!(comment_contains_code("#print(1)", &[]));
assert!(!comment_contains_code("#print", &[]));
assert!(!comment_contains_code("#print 1", &[]));
assert!(!comment_contains_code("#to print", &[]));
}

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -47,6 +47,10 @@ impl Violation for SixPY3 {
/// YTT202
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::SIX) {
return;
}
if checker
.semantic()
.resolve_call_path(expr)

View File

@@ -3,6 +3,7 @@ use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -42,17 +43,19 @@ impl Violation for BlockingOsCallInAsyncFunction {
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_call_path(call.func.as_ref())
.as_ref()
.is_some_and(is_unsafe_os_method)
{
checker.diagnostics.push(Diagnostic::new(
BlockingOsCallInAsyncFunction,
call.func.range(),
));
if checker.semantic().seen_module(Modules::OS) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_call_path(call.func.as_ref())
.as_ref()
.is_some_and(is_unsafe_os_method)
{
checker.diagnostics.push(Diagnostic::new(
BlockingOsCallInAsyncFunction,
call.func.range(),
));
}
}
}
}

View File

@@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -60,6 +60,10 @@ enum Reason {
/// S103
pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::OS) {
return;
}
if checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -35,6 +36,10 @@ impl Violation for DjangoRawSql {
/// S611
pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -35,6 +36,10 @@ impl Violation for LoggingConfigInsecureListen {
/// S612
pub(crate) fn logging_config_insecure_listen(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::LOGGING) {
return;
}
if checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -3,6 +3,7 @@ use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
/// ## What it does
@@ -48,6 +49,10 @@ impl Violation for TarfileUnsafeMembers {
/// S202
pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::TARFILE) {
return;
}
if !call
.func
.as_attribute_expr()
@@ -65,10 +70,6 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall
return;
}
if !checker.semantic().seen(&["tarfile"]) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(TarfileUnsafeMembers, call.func.range()));

View File

@@ -19,7 +19,7 @@ use crate::checkers::ast::Checker;
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
///
/// dsa.generate_private_key(key_size=512)
/// ec.generate_private_key(curve=ec.SECT163K1)
/// ec.generate_private_key(curve=ec.SECT163K1())
/// ```
///
/// Use instead:
@@ -27,7 +27,7 @@ use crate::checkers::ast::Checker;
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
///
/// dsa.generate_private_key(key_size=4096)
/// ec.generate_private_key(curve=ec.SECP384R1)
/// ec.generate_private_key(curve=ec.SECP384R1())
/// ```
///
/// ## References

View File

@@ -191,6 +191,11 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel)
}
// Ex) `typing.Union[bool, int]`
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
// If the typing modules were never imported, we'll never match below.
if !semantic.seen_typing() {
return false;
}
let call_path = semantic.resolve_call_path(value);
if call_path
.as_ref()

View File

@@ -83,7 +83,6 @@ impl Violation for MutableArgumentDefault {
/// B006
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
// Scan in reverse order to right-align zip().
for ParameterWithDefault {
parameter,
default,

View File

@@ -4,6 +4,7 @@ use ruff_python_ast::{self as ast};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -56,6 +57,10 @@ impl Violation for ReSubPositionalArgs {
/// B034
pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::RE) {
return;
}
let Some(method) = checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -1,16 +1,17 @@
use std::iter;
use anyhow::{bail, Result};
use itertools::Itertools;
use libcst_native::{
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
use std::iter;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Stylist;
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
@@ -24,77 +25,10 @@ use crate::{
checkers::ast::Checker,
cst::matchers::{
match_arg, match_call, match_call_mut, match_expression, match_generator_exp, match_lambda,
match_list_comp, match_name, match_tuple,
match_list_comp, match_tuple,
},
};
/// (C400) Convert `list(x for x in y)` to `[x for x in y]`.
pub(crate) fn fix_unnecessary_generator_list(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let generator_exp = match_generator_exp(&arg.value)?;
tree = Expression::ListComp(Box::new(ListComp {
elt: generator_exp.elt.clone(),
for_in: generator_exp.for_in.clone(),
lbracket: LeftSquareBracket {
whitespace_after: call.whitespace_before_args.clone(),
},
rbracket: RightSquareBracket {
whitespace_before: arg.whitespace_after_arg.clone(),
},
lpar: generator_exp.lpar.clone(),
rpar: generator_exp.rpar.clone(),
}));
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
expr.range(),
))
}
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_generator_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let generator_exp = match_generator_exp(&arg.value)?;
tree = Expression::SetComp(Box::new(SetComp {
elt: generator_exp.elt.clone(),
for_in: generator_exp.for_in.clone(),
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: arg.whitespace_after_arg.clone(),
},
lpar: generator_exp.lpar.clone(),
rpar: generator_exp.rpar.clone(),
}));
let content = tree.codegen_stylist(stylist);
Ok(Edit::range_replacement(
pad_expression(content, expr.range(), checker.locator(), checker.semantic()),
expr.range(),
))
}
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
/// range(3)}`.
pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
@@ -151,46 +85,6 @@ pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) ->
))
}
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_list_comprehension_set(
expr: &Expr,
checker: &Checker,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(ListComp)))) ->
// Expr(SetComp)))
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let list_comp = match_list_comp(&arg.value)?;
tree = Expression::SetComp(Box::new(SetComp {
elt: list_comp.elt.clone(),
for_in: list_comp.for_in.clone(),
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: arg.whitespace_after_arg.clone(),
},
lpar: list_comp.lpar.clone(),
rpar: list_comp.rpar.clone(),
}));
Ok(Edit::range_replacement(
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
/// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in
/// range(3)}`.
pub(crate) fn fix_unnecessary_list_comprehension_dict(
@@ -251,95 +145,6 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
))
}
/// Drop a trailing comma from a list of tuple elements.
fn drop_trailing_comma<'a>(
tuple: &Tuple<'a>,
) -> Result<(
Vec<Element<'a>>,
ParenthesizableWhitespace<'a>,
ParenthesizableWhitespace<'a>,
)> {
let whitespace_after = tuple
.lpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_after
.clone();
let whitespace_before = tuple
.rpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_before
.clone();
let mut elements = tuple.elements.clone();
if elements.len() == 1 {
if let Some(Element::Simple {
value,
comma: Some(..),
..
}) = elements.last()
{
if whitespace_before == ParenthesizableWhitespace::default()
&& whitespace_after == ParenthesizableWhitespace::default()
{
elements[0] = Element::Simple {
value: value.clone(),
comma: None,
};
}
}
}
Ok((elements, whitespace_after, whitespace_before))
}
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
pub(crate) fn fix_unnecessary_literal_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call(List|Tuple)))) -> Expr(Set)))
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let (elements, whitespace_after, whitespace_before) = match &arg.value {
Expression::Tuple(inner) => drop_trailing_comma(inner)?,
Expression::List(inner) => (
inner.elements.clone(),
inner.lbracket.whitespace_after.clone(),
inner.rbracket.whitespace_before.clone(),
),
_ => {
bail!("Expected Expression::Tuple | Expression::List");
}
};
if elements.is_empty() {
call.args = vec![];
} else {
tree = Expression::Set(Box::new(Set {
elements,
lbrace: LeftCurlyBrace { whitespace_after },
rbrace: RightCurlyBrace { whitespace_before },
lpar: vec![],
rpar: vec![],
}));
}
Ok(Edit::range_replacement(
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
@@ -408,126 +213,82 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
))
}
/// (C408)
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> {
enum Collection {
Tuple,
List,
Dict,
}
/// (C408) Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
pub(crate) fn fix_unnecessary_collection_call(
expr: &ast::ExprCall,
checker: &Checker,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
// Expr(Call("dict")))) -> Expr(Dict)
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call(&tree)?;
let name = match_name(&call.func)?;
let collection = match name.value {
"tuple" => Collection::Tuple,
"list" => Collection::List,
"dict" => Collection::Dict,
_ => bail!("Expected 'tuple', 'list', or 'dict'"),
};
// Arena allocator used to create formatted strings of sufficient lifetime,
// below.
let mut arena: Vec<String> = vec![];
match collection {
Collection::Tuple => {
tree = Expression::Tuple(Box::new(Tuple {
elements: vec![],
lpar: vec![LeftParen::default()],
rpar: vec![RightParen::default()],
}));
}
Collection::List => {
tree = Expression::List(Box::new(List {
elements: vec![],
lbracket: LeftSquareBracket::default(),
rbracket: RightSquareBracket::default(),
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
// Quote each argument.
for arg in &call.args {
let quoted = format!(
"{}{}{}",
quote,
arg.keyword
.as_ref()
.expect("Expected dictionary argument to be kwarg")
.value,
quote,
);
arena.push(quoted);
}
let elements = call
.args
.iter()
.enumerate()
.map(|(i, arg)| DictElement::Simple {
key: Expression::SimpleString(Box::new(SimpleString {
value: &arena[i],
lpar: vec![],
rpar: vec![],
}));
}
Collection::Dict => {
if call.args.is_empty() {
tree = Expression::Dict(Box::new(Dict {
elements: vec![],
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
lpar: vec![],
rpar: vec![],
}));
} else {
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
})),
value: arg.value.clone(),
comma: arg.comma.clone(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
})
.collect();
// Quote each argument.
for arg in &call.args {
let quoted = format!(
"{}{}{}",
quote,
arg.keyword
.as_ref()
.expect("Expected dictionary argument to be kwarg")
.value,
quote,
);
arena.push(quoted);
}
let elements = call
.args
.iter()
.enumerate()
.map(|(i, arg)| DictElement::Simple {
key: Expression::SimpleString(Box::new(SimpleString {
value: &arena[i],
lpar: vec![],
rpar: vec![],
})),
value: arg.value.clone(),
comma: arg.comma.clone(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
SimpleWhitespace(" "),
),
})
.collect();
tree = Expression::Dict(Box::new(Dict {
elements,
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: call
.args
.last()
.expect("Arguments should be non-empty")
.whitespace_after_arg
.clone(),
},
lpar: vec![],
rpar: vec![],
}));
}
}
};
tree = Expression::Dict(Box::new(Dict {
elements,
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: call
.args
.last()
.expect("Arguments should be non-empty")
.whitespace_after_arg
.clone(),
},
lpar: vec![],
rpar: vec![],
}));
Ok(Edit::range_replacement(
if matches!(collection, Collection::Dict) {
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
)
} else {
tree.codegen_stylist(stylist)
},
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
@@ -544,7 +305,7 @@ pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) ->
/// However, this is a syntax error under the f-string grammar. As such,
/// this method will pad the start and end of an expression as needed to
/// avoid producing invalid syntax.
fn pad_expression(
pub(crate) fn pad_expression(
content: String,
range: TextRange,
locator: &Locator,
@@ -575,106 +336,46 @@ fn pad_expression(
}
}
/// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)`
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
expr: &Expr,
/// Like [`pad_expression`], but only pads the start of the expression.
pub(crate) fn pad_start(
content: &str,
range: TextRange,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let (elements, whitespace_after, whitespace_before) = match &arg.value {
Expression::Tuple(inner) => (
&inner.elements,
&inner
.lpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_after,
&inner
.rpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_before,
),
Expression::List(inner) => (
&inner.elements,
&inner.lbracket.whitespace_after,
&inner.rbracket.whitespace_before,
),
_ => {
bail!("Expected Expression::Tuple | Expression::List");
}
};
semantic: &SemanticModel,
) -> String {
if !semantic.in_f_string() {
return content.into();
}
tree = Expression::Tuple(Box::new(Tuple {
elements: elements.clone(),
lpar: vec![LeftParen {
whitespace_after: whitespace_after.clone(),
}],
rpar: vec![RightParen {
whitespace_before: whitespace_before.clone(),
}],
}));
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
expr.range(),
))
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let prefix = locator.up_to(range.start());
if matches!(prefix.chars().next_back(), Some('{')) {
format!(" {content}")
} else {
content.into()
}
}
/// (C410) Convert `list([1, 2])` to `[1, 2]`
pub(crate) fn fix_unnecessary_literal_within_list_call(
expr: &Expr,
/// Like [`pad_expression`], but only pads the end of the expression.
pub(crate) fn pad_end(
content: &str,
range: TextRange,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let (elements, whitespace_after, whitespace_before) = match &arg.value {
Expression::Tuple(inner) => (
&inner.elements,
&inner
.lpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_after,
&inner
.rpar
.first()
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
.whitespace_before,
),
Expression::List(inner) => (
&inner.elements,
&inner.lbracket.whitespace_after,
&inner.rbracket.whitespace_before,
),
_ => {
bail!("Expected Expression::Tuple | Expression::List");
}
};
semantic: &SemanticModel,
) -> String {
if !semantic.in_f_string() {
return content.into();
}
tree = Expression::List(Box::new(List {
elements: elements.clone(),
lbracket: LeftSquareBracket {
whitespace_after: whitespace_after.clone(),
},
rbracket: RightSquareBracket {
whitespace_before: whitespace_before.clone(),
},
lpar: vec![],
rpar: vec![],
}));
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
expr.range(),
))
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let suffix = locator.after(range.end());
if matches!(suffix.chars().next(), Some('}')) {
format!("{content} ")
} else {
content.into()
}
}
/// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`.
@@ -1091,25 +792,6 @@ pub(crate) fn fix_unnecessary_map(
Ok(Edit::range_replacement(content, expr.range()))
}
/// (C418) Convert `dict({"a": 1})` to `{"a": 1}`
pub(crate) fn fix_unnecessary_literal_within_dict_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
tree = arg.value.clone();
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
expr.range(),
))
}
/// (C419) Convert `[i for i in a]` into `i for i in a`
pub(crate) fn fix_unnecessary_comprehension_any_all(
expr: &Expr,

View File

@@ -1,11 +1,11 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword};
use ruff_text_size::Ranged;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use crate::rules::flake8_comprehensions::settings::Settings;
/// ## What it does
@@ -36,6 +36,9 @@ use crate::rules::flake8_comprehensions::settings::Settings;
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
///
/// ## Options
/// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments`
#[violation]
pub struct UnnecessaryCollectionCall {
obj_type: String,
@@ -56,42 +59,88 @@ impl AlwaysFixableViolation for UnnecessaryCollectionCall {
/// C408
pub(crate) fn unnecessary_collection_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
call: &ast::ExprCall,
settings: &Settings,
) {
if !args.is_empty() {
if !call.arguments.args.is_empty() {
return;
}
let Some(func) = func.as_name_expr() else {
let Some(func) = call.func.as_name_expr() else {
return;
};
match func.id.as_str() {
let collection = match func.id.as_str() {
"dict"
if keywords.is_empty()
if call.arguments.keywords.is_empty()
|| (!settings.allow_dict_calls_with_keyword_arguments
&& keywords.iter().all(|kw| kw.arg.is_some())) =>
&& call.arguments.keywords.iter().all(|kw| kw.arg.is_some())) =>
{
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
Collection::Dict
}
"list" | "tuple" => {
// `list()` or `tuple()`
"list" if call.arguments.keywords.is_empty() => {
// `list()
Collection::List
}
"tuple" if call.arguments.keywords.is_empty() => {
// `tuple()`
Collection::Tuple
}
_ => return,
};
if !checker.semantic().is_builtin(func.id.as_str()) {
return;
}
let mut diagnostic = Diagnostic::new(
UnnecessaryCollectionCall {
obj_type: func.id.to_string(),
},
expr.range(),
call.range(),
);
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(expr, checker).map(Fix::unsafe_edit)
});
// Convert `dict()` to `{}`.
if call.arguments.keywords.is_empty() {
diagnostic.set_fix({
// Replace from the start of the call to the start of the argument.
let call_start = Edit::replacement(
match collection {
Collection::Dict => {
pad_start("{", call.range(), checker.locator(), checker.semantic())
}
Collection::List => "[".to_string(),
Collection::Tuple => "(".to_string(),
},
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace from the end of the inner list or tuple to the end of the call with `}`.
let call_end = Edit::replacement(
match collection {
Collection::Dict => {
pad_end("}", call.range(), checker.locator(), checker.semantic())
}
Collection::List => "]".to_string(),
Collection::Tuple => ")".to_string(),
},
call.arguments.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
});
} else {
// Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(call, checker).map(Fix::unsafe_edit)
});
}
checker.diagnostics.push(diagnostic);
}
enum Collection {
Tuple,
List,
Dict,
}

View File

@@ -1,13 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
@@ -47,27 +44,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorList {
}
/// C400 (`list(generator)`)
pub(crate) fn unnecessary_generator_list(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("list", func, args, keywords)
else {
pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::ExprCall) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
"list",
&call.func,
&call.arguments.args,
&call.arguments.keywords,
) else {
return;
};
if !checker.semantic().is_builtin("list") {
return;
}
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_list(expr, checker.locator(), checker.stylist())
.map(Fix::unsafe_edit)
if argument.is_generator_exp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, call.range());
// Convert `list(x for x in y)` to `[x for x in y]`.
diagnostic.set_fix({
// Replace `list(` with `[`.
let call_start = Edit::replacement(
"[".to_string(),
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace `)` with `]`.
let call_end = Edit::replacement(
"]".to_string(),
call.arguments.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
});
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,12 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use super::helpers;
@@ -47,26 +45,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorSet {
}
/// C401 (`set(generator)`)
pub(crate) fn unnecessary_generator_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprCall) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
"set",
&call.func,
&call.arguments.args,
&call.arguments.keywords,
) else {
return;
};
if !checker.semantic().is_builtin("set") {
return;
}
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_set(expr, checker).map(Fix::unsafe_edit)
if argument.is_generator_exp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, call.range());
// Convert `set(x for x in y)` to `{x for x in y}`.
diagnostic.set_fix({
// Replace `set(` with `}`.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace `)` with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
call.arguments.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
});
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,12 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use super::helpers;
@@ -45,25 +43,43 @@ impl AlwaysFixableViolation for UnnecessaryListComprehensionSet {
}
/// C403 (`set([...])`)
pub(crate) fn unnecessary_list_comprehension_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
pub(crate) fn unnecessary_list_comprehension_set(checker: &mut Checker, call: &ast::ExprCall) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
"set",
&call.func,
&call.arguments.args,
&call.arguments.keywords,
) else {
return;
};
if !checker.semantic().is_builtin("set") {
return;
}
if argument.is_list_comp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_set(expr, checker).map(Fix::unsafe_edit)
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range());
diagnostic.set_fix({
// Replace `set(` with `{`.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace `)` with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
call.arguments.end() - TextSize::from(1),
call.end(),
);
// Delete the open bracket (`[`).
let argument_start =
Edit::deletion(argument.start(), argument.start() + TextSize::from(1));
// Delete the close bracket (`]`).
let argument_end = Edit::deletion(argument.end() - TextSize::from(1), argument.end());
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,12 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use super::helpers;
@@ -53,16 +51,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralSet {
}
/// C405 (`set([1, 2])`)
pub(crate) fn unnecessary_literal_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
pub(crate) fn unnecessary_literal_set(checker: &mut Checker, call: &ast::ExprCall) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
"set",
&call.func,
&call.arguments.args,
&call.arguments.keywords,
) else {
return;
};
if !checker.semantic().is_builtin("set") {
@@ -73,13 +68,69 @@ pub(crate) fn unnecessary_literal_set(
Expr::Tuple(_) => "tuple",
_ => return,
};
let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralSet {
obj_type: kind.to_string(),
},
expr.range(),
call.range(),
);
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_set(expr, checker).map(Fix::unsafe_edit));
// Convert `set((1, 2))` to `{1, 2}`.
diagnostic.set_fix({
let elts = match &argument {
Expr::List(ast::ExprList { elts, .. }) => elts,
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts,
_ => unreachable!(),
};
match elts.as_slice() {
// If the list or tuple is empty, replace the entire call with `set()`.
[] => Fix::unsafe_edit(Edit::range_replacement("set()".to_string(), call.range())),
// If it's a single-element tuple (with no whitespace around it), remove the trailing
// comma.
[elt]
if argument.is_tuple_expr()
// The element must start right after the `(`.
&& elt.start() == argument.start() + TextSize::new(1)
// The element must be followed by exactly one comma and a closing `)`.
&& elt.end() + TextSize::new(2) == argument.end() =>
{
// Replace from the start of the call to the start of the inner element.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
elt.start(),
);
// Replace from the end of the inner element to the end of the call with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
elt.end(),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
}
_ => {
// Replace from the start of the call to the start of the inner list or tuple with `{`.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
argument.start() + TextSize::from(1),
);
// Replace from the end of the inner list or tuple to the end of the call with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
argument.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
}
}
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,15 +1,13 @@
use std::fmt;
use ruff_python_ast::{Expr, Keyword};
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
#[derive(Debug, PartialEq, Eq)]
@@ -68,17 +66,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinDictCall {
}
/// C418
pub(crate) fn unnecessary_literal_within_dict_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if !keywords.is_empty() {
pub(crate) fn unnecessary_literal_within_dict_call(checker: &mut Checker, call: &ast::ExprCall) {
if !call.arguments.keywords.is_empty() {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("dict", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("dict", &call.func, &call.arguments.args)
else {
return;
};
if !checker.semantic().is_builtin("dict") {
@@ -89,15 +83,24 @@ pub(crate) fn unnecessary_literal_within_dict_call(
Expr::Dict(_) => DictKind::Literal,
_ => return,
};
let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralWithinDictCall {
kind: argument_kind,
},
expr.range(),
call.range(),
);
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_dict_call(expr, checker.locator(), checker.stylist())
.map(Fix::unsafe_edit)
// Convert `dict({"a": 1})` to `{"a": 1}`
diagnostic.set_fix({
// Delete from the start of the call to the start of the argument.
let call_start = Edit::deletion(call.start(), argument.start());
// Delete from the end of the argument to the end of the call.
let call_end = Edit::deletion(argument.end(), call.end());
Fix::unsafe_edits(call_start, [call_end])
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,12 +1,10 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword};
use ruff_text_size::Ranged;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
@@ -70,17 +68,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinListCall {
}
/// C410
pub(crate) fn unnecessary_literal_within_list_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if !keywords.is_empty() {
pub(crate) fn unnecessary_literal_within_list_call(checker: &mut Checker, call: &ast::ExprCall) {
if !call.arguments.keywords.is_empty() {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("list", &call.func, &call.arguments.args)
else {
return;
};
if !checker.semantic().is_builtin("list") {
@@ -91,15 +85,43 @@ pub(crate) fn unnecessary_literal_within_list_call(
Expr::List(_) => "list",
_ => return,
};
let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralWithinListCall {
literal: argument_kind.to_string(),
},
expr.range(),
call.range(),
);
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_list_call(expr, checker.locator(), checker.stylist())
.map(Fix::unsafe_edit)
// Convert `list([1, 2])` to `[1, 2]`
diagnostic.set_fix({
// Delete from the start of the call to the start of the argument.
let call_start = Edit::deletion(call.start(), argument.start());
// Delete from the end of the argument to the end of the call.
let call_end = Edit::deletion(argument.end(), call.end());
// If this is a tuple, we also need to convert the inner argument to a list.
if argument.is_tuple_expr() {
// Replace `(` with `[`.
let argument_start = Edit::replacement(
"[".to_string(),
argument.start(),
argument.start() + TextSize::from(1),
);
// Replace `)` with `]`.
let argument_end = Edit::replacement(
"]".to_string(),
argument.end() - TextSize::from(1),
argument.end(),
);
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
} else {
Fix::unsafe_edits(call_start, [call_end])
}
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,13 +1,10 @@
use ruff_python_ast::{Expr, Keyword};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
@@ -48,13 +45,11 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinTupleCall {
let UnnecessaryLiteralWithinTupleCall { literal } = self;
if literal == "list" {
format!(
"Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` \
literal)"
"Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` literal)"
)
} else {
format!(
"Unnecessary `{literal}` literal passed to `tuple()` (remove the outer call to \
`tuple()`)"
"Unnecessary `{literal}` literal passed to `tuple()` (remove the outer call to `tuple()`)"
)
}
}
@@ -72,17 +67,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinTupleCall {
}
/// C409
pub(crate) fn unnecessary_literal_within_tuple_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if !keywords.is_empty() {
pub(crate) fn unnecessary_literal_within_tuple_call(checker: &mut Checker, call: &ast::ExprCall) {
if !call.arguments.keywords.is_empty() {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("tuple", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("tuple", &call.func, &call.arguments.args)
else {
return;
};
if !checker.semantic().is_builtin("tuple") {
@@ -93,15 +84,32 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
Expr::List(_) => "list",
_ => return,
};
let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralWithinTupleCall {
literal: argument_kind.to_string(),
},
expr.range(),
call.range(),
);
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_tuple_call(expr, checker.locator(), checker.stylist())
.map(Fix::unsafe_edit)
// Convert `tuple([1, 2])` to `tuple(1, 2)`
diagnostic.set_fix({
// Replace from the start of the call to the start of the inner list or tuple with `(`.
let call_start = Edit::replacement(
"(".to_string(),
call.start(),
argument.start() + TextSize::from(1),
);
// Replace from the end of the inner list or tuple to the end of the call with `)`.
let call_end = Edit::replacement(
")".to_string(),
argument.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -120,6 +120,8 @@ C403.py:14:9: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comp
14 |-s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
14 |+s = f"{ {x for x in 'ab'} | set([x for x in 'ab']) }"
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
16 16 |
17 17 | s = set( # comment
C403.py:14:34: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
@@ -138,12 +140,16 @@ C403.py:14:34: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` com
14 |-s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
14 |+s = f"{ set([x for x in 'ab']) | {x for x in 'ab'} }"
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
16 16 |
17 17 | s = set( # comment
C403.py:15:8: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
| ^^^^^^^^^^^^^^^^^^^^^^ C403
16 |
17 | s = set( # comment
|
= help: Rewrite as a `set` comprehension
@@ -153,12 +159,17 @@ C403.py:15:8: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comp
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
15 |-s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
15 |+s = f"{ {x for x in 'ab'} | set([x for x in 'ab'])}"
16 16 |
17 17 | s = set( # comment
18 18 | [x for x in range(3)]
C403.py:15:33: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
| ^^^^^^^^^^^^^^^^^^^^^^ C403
16 |
17 | s = set( # comment
|
= help: Rewrite as a `set` comprehension
@@ -168,5 +179,58 @@ C403.py:15:33: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` com
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
15 |-s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
15 |+s = f"{set([x for x in 'ab']) | {x for x in 'ab'} }"
16 16 |
17 17 | s = set( # comment
18 18 | [x for x in range(3)]
C403.py:17:5: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
16 |
17 | s = set( # comment
| _____^
18 | | [x for x in range(3)]
19 | | )
| |_^ C403
20 |
21 | s = set([ # comment
|
= help: Rewrite as a `set` comprehension
Unsafe fix
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
16 16 |
17 |-s = set( # comment
18 |- [x for x in range(3)]
19 |-)
17 |+s = { # comment
18 |+ x for x in range(3)
19 |+}
20 20 |
21 21 | s = set([ # comment
22 22 | x for x in range(3)
C403.py:21:5: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
19 | )
20 |
21 | s = set([ # comment
| _____^
22 | | x for x in range(3)
23 | | ])
| |__^ C403
|
= help: Rewrite as a `set` comprehension
Unsafe fix
18 18 | [x for x in range(3)]
19 19 | )
20 20 |
21 |-s = set([ # comment
21 |+s = { # comment
22 22 | x for x in range(3)
23 |-])
23 |+}

View File

@@ -217,6 +217,7 @@ C408.py:20:5: C408 [*] Unnecessary `dict` call (rewrite as a literal)
20 |+f"{ {'x': 'y'} | dict(y='z') }"
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
@@ -236,6 +237,7 @@ C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
20 |+f"{ dict(x='y') | {'y': 'z'} }"
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
@@ -254,6 +256,8 @@ C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 |-f"a {dict(x='y') | dict(y='z')} b"
21 |+f"a { {'x': 'y'} | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 24 | dict(
C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
@@ -272,6 +276,8 @@ C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 |-f"a {dict(x='y') | dict(y='z')} b"
21 |+f"a {dict(x='y') | {'y': 'z'} } b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 24 | dict(
C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
@@ -279,6 +285,8 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 | f"a {dict(x='y') | dict(y='z')} b"
22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408
23 |
24 | dict(
|
= help: Rewrite as a literal
@@ -288,6 +296,9 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 |-f"a { dict(x='y') | dict(y='z') } b"
22 |+f"a { {'x': 'y'} | dict(y='z') } b"
23 23 |
24 24 | dict(
25 25 | # comment
C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
@@ -295,6 +306,8 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 | f"a {dict(x='y') | dict(y='z')} b"
22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408
23 |
24 | dict(
|
= help: Rewrite as a literal
@@ -304,5 +317,52 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 |-f"a { dict(x='y') | dict(y='z') } b"
22 |+f"a { dict(x='y') | {'y': 'z'} } b"
23 23 |
24 24 | dict(
25 25 | # comment
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
22 | f"a { dict(x='y') | dict(y='z') } b"
23 |
24 | / dict(
25 | | # comment
26 | | )
| |_^ C408
27 |
28 | tuple( # comment
|
= help: Rewrite as a literal
Unsafe fix
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 |-dict(
24 |+{
25 25 | # comment
26 |-)
26 |+}
27 27 |
28 28 | tuple( # comment
29 29 | )
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
26 | )
27 |
28 | / tuple( # comment
29 | | )
| |_^ C408
|
= help: Rewrite as a literal
Unsafe fix
25 25 | # comment
26 26 | )
27 27 |
28 |-tuple( # comment
28 |+( # comment
29 29 | )

View File

@@ -96,4 +96,48 @@ C408.py:17:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
19 19 | f"{dict(x='y') | dict(y='z')}"
20 20 | f"{ dict(x='y') | dict(y='z') }"
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
22 | f"a { dict(x='y') | dict(y='z') } b"
23 |
24 | / dict(
25 | | # comment
26 | | )
| |_^ C408
27 |
28 | tuple( # comment
|
= help: Rewrite as a literal
Unsafe fix
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 |-dict(
24 |+{
25 25 | # comment
26 |-)
26 |+}
27 27 |
28 28 | tuple( # comment
29 29 | )
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
26 | )
27 |
28 | / tuple( # comment
29 | | )
| |_^ C408
|
= help: Rewrite as a literal
Unsafe fix
25 25 | # comment
26 26 | )
27 27 |
28 |-tuple( # comment
28 |+( # comment
29 29 | )

View File

@@ -93,16 +93,67 @@ C409.py:8:6: C409 [*] Unnecessary `tuple` literal passed to `tuple()` (remove th
9 | | (1, 2)
10 | | )
| |_^ C409
11 |
12 | tuple( # comment
|
= help: Remove outer `tuple` call
Unsafe fix
5 5 | 1,
6 6 | 2
7 7 | ])
8 |-t5 = tuple(
9 |- (1, 2)
10 |-)
8 |+t5 = (1, 2)
5 5 | 1,
6 6 | 2
7 7 | ])
8 |-t5 = tuple(
9 |- (1, 2)
10 |-)
8 |+t5 = (1, 2)
11 9 |
12 10 | tuple( # comment
13 11 | [1, 2]
C409.py:12:1: C409 [*] Unnecessary `list` literal passed to `tuple()` (rewrite as a `tuple` literal)
|
10 | )
11 |
12 | / tuple( # comment
13 | | [1, 2]
14 | | )
| |_^ C409
15 |
16 | tuple([ # comment
|
= help: Rewrite as a `tuple` literal
Unsafe fix
9 9 | (1, 2)
10 10 | )
11 11 |
12 |-tuple( # comment
13 |- [1, 2]
14 |-)
12 |+(1, 2)
15 13 |
16 14 | tuple([ # comment
17 15 | 1, 2
C409.py:16:1: C409 [*] Unnecessary `list` literal passed to `tuple()` (rewrite as a `tuple` literal)
|
14 | )
15 |
16 | / tuple([ # comment
17 | | 1, 2
18 | | ])
| |__^ C409
|
= help: Rewrite as a `tuple` literal
Unsafe fix
13 13 | [1, 2]
14 14 | )
15 15 |
16 |-tuple([ # comment
16 |+( # comment
17 17 | 1, 2
18 |-])
18 |+)

View File

@@ -33,6 +33,7 @@ C410.py:2:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as
2 |+l2 = [1, 2]
3 3 | l3 = list([])
4 4 | l4 = list(())
5 5 |
C410.py:3:6: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
@@ -50,6 +51,8 @@ C410.py:3:6: C410 [*] Unnecessary `list` literal passed to `list()` (remove the
3 |-l3 = list([])
3 |+l3 = []
4 4 | l4 = list(())
5 5 |
6 6 |
C410.py:4:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as a `list` literal)
|
@@ -66,5 +69,52 @@ C410.py:4:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as
3 3 | l3 = list([])
4 |-l4 = list(())
4 |+l4 = []
5 5 |
6 6 |
7 7 | list( # comment
C410.py:7:1: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
7 | / list( # comment
8 | | [1, 2]
9 | | )
| |_^ C410
10 |
11 | list([ # comment
|
= help: Remove outer `list` call
Unsafe fix
4 4 | l4 = list(())
5 5 |
6 6 |
7 |-list( # comment
8 |- [1, 2]
9 |-)
7 |+[1, 2]
10 8 |
11 9 | list([ # comment
12 10 | 1, 2
C410.py:11:1: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
9 | )
10 |
11 | / list([ # comment
12 | | 1, 2
13 | | ])
| |__^ C410
|
= help: Remove outer `list` call
Unsafe fix
8 8 | [1, 2]
9 9 | )
10 10 |
11 |-list([ # comment
11 |+[ # comment
12 12 | 1, 2
13 |-])
13 |+]

View File

@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -57,6 +58,10 @@ impl Violation for CallDateFromtimestamp {
}
pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if checker
.semantic()
.resolve_call_path(func)

View File

@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -56,6 +57,10 @@ impl Violation for CallDateToday {
}
pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if checker
.semantic()
.resolve_call_path(func)

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -60,6 +61,10 @@ impl Violation for CallDatetimeFromtimestamp {
}
pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -56,6 +57,10 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
}
pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -64,6 +65,10 @@ impl Violation for CallDatetimeStrptimeWithoutZone {
/// DTZ007
pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -55,6 +56,10 @@ impl Violation for CallDatetimeToday {
}
pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(func)

View File

@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -63,6 +64,10 @@ pub(crate) fn call_datetime_utcfromtimestamp(
func: &Expr,
location: TextRange,
) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(func)

View File

@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -57,7 +58,12 @@ impl Violation for CallDatetimeUtcnow {
}
}
/// DTZ003
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(func)

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -52,6 +53,10 @@ impl Violation for CallDatetimeWithoutTzinfo {
}
pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;
}
if !checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -48,6 +49,10 @@ impl Violation for DjangoAllWithModelForm {
/// DJ007
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if !is_model_form(class_def, checker.semantic()) {
return;
}

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -46,6 +47,10 @@ impl Violation for DjangoExcludeWithModelForm {
/// DJ006
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if !is_model_form(class_def, checker.semantic()) {
return;
}

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,6 +45,10 @@ impl Violation for DjangoLocalsInRenderFunction {
/// DJ003
pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if !checker
.semantic()
.resolve_call_path(&call.func)

View File

@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::{analyze, SemanticModel};
use ruff_python_semantic::{analyze, Modules, SemanticModel};
use crate::checkers::ast::Checker;
@@ -52,6 +52,10 @@ impl Violation for DjangoModelWithoutDunderStr {
/// DJ008
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if !is_non_abstract_model(class_def, checker.semantic()) {
return;
}

View File

@@ -2,6 +2,7 @@ use ruff_python_ast::Decorator;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -51,6 +52,10 @@ impl Violation for DjangoNonLeadingReceiverDecorator {
/// DJ013
pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
let mut seen_receiver = false;
for (i, decorator) in decorator_list.iter().enumerate() {
let is_receiver = decorator.expression.as_call_expr().is_some_and(|call| {

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -55,6 +55,10 @@ impl Violation for DjangoNullableModelStringField {
/// DJ001
pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
for statement in body {
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
continue;

View File

@@ -3,7 +3,7 @@ use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -83,6 +83,10 @@ pub(crate) fn unordered_body_content_in_model(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
) {
if !checker.semantic().seen_module(Modules::DJANGO) {
return;
}
if !helpers::is_model(class_def, checker.semantic()) {
return;
}

View File

@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -101,28 +101,37 @@ pub(crate) fn implicit(
for ((a_tok, a_range), (b_tok, b_range)) in tokens
.iter()
.flatten()
.filter(|(tok, _)| {
!tok.is_comment()
&& (settings.flake8_implicit_str_concat.allow_multiline
|| !tok.is_non_logical_newline())
.filter_map(|(tok, range)| {
// Ignore comments.
if tok.is_comment() {
return None;
}
// Ignore non-localized newlines.
if !settings.flake8_implicit_str_concat.allow_multiline && tok.is_non_logical_newline()
{
return None;
}
Some((TokenKind::from(tok), *range))
})
.tuple_windows()
{
let (a_range, b_range) = match (a_tok, b_tok) {
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
(Tok::String { .. }, Tok::FStringStart) => {
(TokenKind::String, TokenKind::String) => (a_range, b_range),
(TokenKind::String, TokenKind::FStringStart) => {
match indexer.fstring_ranges().innermost(b_range.start()) {
Some(b_range) => (*a_range, b_range),
Some(b_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::String { .. }) => {
(TokenKind::FStringEnd, TokenKind::String { .. }) => {
match indexer.fstring_ranges().innermost(a_range.start()) {
Some(a_range) => (a_range, *b_range),
Some(a_range) => (a_range, b_range),
None => continue,
}
}
(Tok::FStringEnd, Tok::FStringStart) => {
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
match (
indexer.fstring_ranges().innermost(a_range.start()),
indexer.fstring_ranges().innermost(b_range.start()),

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -54,6 +55,10 @@ impl Violation for DirectLoggerInstantiation {
/// LOG001
pub(crate) fn direct_logger_instantiation(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::LOGGING) {
return;
}
if checker
.semantic()
.resolve_call_path(call.func.as_ref())

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -57,6 +58,10 @@ impl Violation for InvalidGetLoggerArgument {
/// LOG002
pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::LOGGING) {
return;
}
let Some(Expr::Name(expr @ ast::ExprName { id, .. })) = call.arguments.find_argument("name", 0)
else {
return;

View File

@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -48,6 +49,10 @@ impl Violation for UndocumentedWarn {
/// LOG009
pub(crate) fn undocumented_warn(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::LOGGING) {
return;
}
if checker
.semantic()
.resolve_call_path(expr)

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::AlwaysFixableViolation;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::Stmt;
use ruff_python_semantic::{ScopeKind, SemanticModel};
@@ -132,7 +133,7 @@ fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
ScopeKind::Class(class_def) => class_def
.bases()
.iter()
.any(|base| semantic.match_typing_expr(base, "Protocol")),
.any(|base| semantic.match_typing_expr(map_subscript(base), "Protocol")),
ScopeKind::Function(function_def) => {
ruff_python_semantic::analyze::visibility::is_abstract(
&function_def.decorator_list,

View File

@@ -671,5 +671,8 @@ PIE790.py:209:9: PIE790 [*] Unnecessary `...` literal
207 207 | def stub(self) -> str:
208 208 | """Docstring"""
209 |- ...
210 209 |
211 210 |
212 211 | class Repro(Protocol[int]):

View File

@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -50,6 +51,10 @@ impl Violation for CollectionsNamedTuple {
/// PYI024
pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::COLLECTIONS) {
return;
}
if checker
.semantic()
.resolve_call_path(expr)

View File

@@ -1,9 +1,8 @@
use std::fmt;
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -61,44 +60,53 @@ impl Violation for UnprefixedTypeParam {
/// PYI001
pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
// If the typing modules were never imported, we'll never match below.
if !checker.semantic().seen_typing() {
return;
}
let [target] = targets else {
return;
};
if let Expr::Name(ast::ExprName { id, .. }) = target {
if id.starts_with('_') {
return;
}
};
if let Expr::Call(ast::ExprCall { func, .. }) = value {
let Some(kind) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| {
if checker
.semantic()
.match_typing_call_path(&call_path, "ParamSpec")
{
Some(VarKind::ParamSpec)
} else if checker
.semantic()
.match_typing_call_path(&call_path, "TypeVar")
{
Some(VarKind::TypeVar)
} else if checker
.semantic()
.match_typing_call_path(&call_path, "TypeVarTuple")
{
Some(VarKind::TypeVarTuple)
} else {
None
}
})
else {
return;
};
checker
.diagnostics
.push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range()));
}
let Expr::Call(ast::ExprCall { func, .. }) = value else {
return;
};
let Some(kind) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| {
if checker
.semantic()
.match_typing_call_path(&call_path, "ParamSpec")
{
Some(VarKind::ParamSpec)
} else if checker
.semantic()
.match_typing_call_path(&call_path, "TypeVar")
{
Some(VarKind::TypeVar)
} else if checker
.semantic()
.match_typing_call_path(&call_path, "TypeVarTuple")
{
Some(VarKind::TypeVarTuple)
} else {
None
}
})
else {
return;
};
checker
.diagnostics
.push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range()));
}

View File

@@ -221,6 +221,7 @@ pub(crate) fn avoidable_escaped_quote(
Tok::FStringMiddle {
value: string_contents,
is_raw,
triple_quoted: _,
} if !is_raw => {
let Some(context) = fstrings.last_mut() else {
continue;
@@ -361,6 +362,7 @@ pub(crate) fn unnecessary_escaped_quote(
Tok::FStringMiddle {
value: string_contents,
is_raw,
triple_quoted: _,
} if !is_raw => {
let Some(context) = fstrings.last_mut() else {
continue;

View File

@@ -1,28 +1,27 @@
use anyhow::Result;
use std::ops::Add;
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use anyhow::Result;
use ruff_diagnostics::{AlwaysFixableViolation, FixAvailability, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::stmt_if::elif_else_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::{is_python_whitespace, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::fix::edits;
use crate::fix::edits::adjust_indentation;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_return::helpers::end_of_last_statement;
use crate::rules::pyupgrade::fixes::adjust_indentation;
use super::super::branch::Branch;
use super::super::helpers::result_exists;
@@ -853,6 +852,7 @@ fn remove_else(
TextRange::new(else_colon_end, elif_else.end()),
desired_indentation,
locator,
indexer,
stylist,
)?;

View File

@@ -171,4 +171,48 @@ RET505.py:200:5: RET505 Unnecessary `else` after `return` statement
|
= help: Remove unnecessary `else`
RET505.py:207:5: RET505 Unnecessary `else` after `return` statement
|
205 | a = 1
206 | return y
207 | else:
| ^^^^ RET505
208 |
209 | c = 3
|
= help: Remove unnecessary `else`
RET505.py:217:5: RET505 Unnecessary `else` after `return` statement
|
215 | a = 1
216 | return y
217 | else:
| ^^^^ RET505
218 | # comment
219 | c = 3
|
= help: Remove unnecessary `else`
RET505.py:227:5: RET505 Unnecessary `else` after `return` statement
|
225 | a = 1
226 | return y
227 | else:
| ^^^^ RET505
228 | # comment
229 | c = 3
|
= help: Remove unnecessary `else`
RET505.py:237:5: RET505 Unnecessary `else` after `return` statement
|
235 | a = 1
236 | return y
237 | else:
| ^^^^ RET505
238 | # comment
239 | c = 3
|
= help: Remove unnecessary `else`

View File

@@ -358,5 +358,107 @@ RET505.py:200:5: RET505 [*] Unnecessary `else` after `return` statement
199 199 | if self._sb is not None: return self._sb
200 |- else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
200 |+ self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
201 201 |
202 202 |
203 203 | def indent(x, y, w, z):
RET505.py:207:5: RET505 [*] Unnecessary `else` after `return` statement
|
205 | a = 1
206 | return y
207 | else:
| ^^^^ RET505
208 |
209 | c = 3
|
= help: Remove unnecessary `else`
Safe fix
204 204 | if x: # [no-else-return]
205 205 | a = 1
206 206 | return y
207 |- else:
208 207 |
209 |- c = 3
210 |- return z
208 |+ c = 3
209 |+ return z
211 210 |
212 211 |
213 212 | def indent(x, y, w, z):
RET505.py:217:5: RET505 [*] Unnecessary `else` after `return` statement
|
215 | a = 1
216 | return y
217 | else:
| ^^^^ RET505
218 | # comment
219 | c = 3
|
= help: Remove unnecessary `else`
Safe fix
214 214 | if x: # [no-else-return]
215 215 | a = 1
216 216 | return y
217 |- else:
218 |- # comment
219 |- c = 3
220 |- return z
217 |+ # comment
218 |+ c = 3
219 |+ return z
221 220 |
222 221 |
223 222 | def indent(x, y, w, z):
RET505.py:227:5: RET505 [*] Unnecessary `else` after `return` statement
|
225 | a = 1
226 | return y
227 | else:
| ^^^^ RET505
228 | # comment
229 | c = 3
|
= help: Remove unnecessary `else`
Safe fix
224 224 | if x: # [no-else-return]
225 225 | a = 1
226 226 | return y
227 |- else:
228 |- # comment
229 |- c = 3
230 |- return z
227 |+ # comment
228 |+ c = 3
229 |+ return z
231 230 |
232 231 |
233 232 | def indent(x, y, w, z):
RET505.py:237:5: RET505 [*] Unnecessary `else` after `return` statement
|
235 | a = 1
236 | return y
237 | else:
| ^^^^ RET505
238 | # comment
239 | c = 3
|
= help: Remove unnecessary `else`
Safe fix
234 234 | if x: # [no-else-return]
235 235 | a = 1
236 236 | return y
237 |- else:
238 237 | # comment
239 |- c = 3
240 |- return z
238 |+ c = 3
239 |+ return z

View File

@@ -5,6 +5,7 @@ use crate::fix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_dict;
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
@@ -121,6 +122,10 @@ fn is_lowercase_allowed(env_var: &str) -> bool {
/// SIM112
pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::OS) {
return;
}
// Ex) `os.environ['foo']`
if let Expr::Subscript(_) = expr {
check_os_environ_subscript(checker, expr);

View File

@@ -58,6 +58,10 @@ pub(crate) fn banned_api<T: Ranged>(checker: &mut Checker, policy: &NameMatchPol
/// TID251
pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) {
let banned_api = &checker.settings.flake8_tidy_imports.banned_api;
if banned_api.is_empty() {
return;
}
if let Some((banned_path, ban)) =
checker
.semantic()

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -48,15 +49,17 @@ pub(crate) fn async_function_with_timeout(
if !function_def.is_async {
return;
}
// If `trio` isn't in scope, avoid raising the diagnostic.
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
// If the function doesn't have a `timeout` parameter, avoid raising the diagnostic.
let Some(timeout) = function_def.parameters.find("timeout") else {
return;
};
// If `trio` isn't in scope, avoid raising the diagnostic.
if !checker.semantic().seen(&["trio"]) {
return;
}
checker.diagnostics.push(Diagnostic::new(
TrioAsyncFunctionWithTimeout,
timeout.range(),

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, ExprCall};
use ruff_python_semantic::Modules;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -51,6 +52,10 @@ impl Violation for TrioSyncCall {
/// TRIO105
pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
let Some(method_name) = ({
let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else {
return;

View File

@@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::AwaitVisitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{StmtWith, WithItem};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
use crate::rules::flake8_trio::method_name::MethodName;
@@ -49,6 +50,10 @@ pub(crate) fn timeout_without_await(
with_stmt: &StmtWith,
with_items: &[WithItem],
) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
let Some(method_name) = with_items.iter().find_map(|item| {
let call = item.context_expr.as_call_expr()?;
let call_path = checker.semantic().resolve_call_path(call.func.as_ref())?;

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -42,6 +43,10 @@ impl Violation for TrioUnneededSleep {
/// TRIO110
pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
// The body should be a single `await` call.
let [stmt] = while_stmt.body.as_slice() else {
return;

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
use ruff_python_semantic::analyze::typing::find_assigned_value;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -47,11 +48,7 @@ impl AlwaysFixableViolation for TrioZeroSleepCall {
/// TRIO115
pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
if !checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"]))
{
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
@@ -63,6 +60,14 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
return;
};
if !checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"]))
{
return;
}
match arg {
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
if !matches!(value, Number::Int(Int::ZERO)) {

View File

@@ -6,7 +6,7 @@ use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast, Decorator, Expr};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_semantic::{
analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel,
analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel,
};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -111,6 +111,10 @@ fn runtime_required_decorators(
///
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_module(Modules::DATACLASSES) {
return false;
}
// Determine whether the assignment is in a `@dataclass` class definition.
if let ScopeKind::Class(class_def) = semantic.current_scope().kind {
if class_def.decorator_list.iter().any(|decorator| {

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprAttribute};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -53,6 +54,10 @@ impl Violation for OsSepSplit {
/// PTH206
pub(crate) fn os_sep_split(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::OS) {
return;
}
let Expr::Attribute(ExprAttribute { attr, .. }) = call.func.as_ref() else {
return;
};

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -55,6 +56,10 @@ impl Violation for NumpyDeprecatedFunction {
/// NPY003
pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::NUMPY) {
return;
}
if let Some((existing, replacement)) =
checker
.semantic()

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -49,6 +50,10 @@ impl Violation for NumpyDeprecatedTypeAlias {
/// NPY001
pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::NUMPY) {
return;
}
if let Some(type_name) = checker
.semantic()
.resolve_call_path(expr)

View File

@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -59,6 +60,10 @@ impl Violation for NumpyLegacyRandom {
/// NPY002
pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::NUMPY) {
return;
}
if let Some(method_name) = checker
.semantic()
.resolve_call_path(expr)

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