Compare commits

...

108 Commits

Author SHA1 Message Date
Carl Meyer
ab9b38c0f1 re-add para on CLICOLOR and CLICOLOR_FORCE 2024-04-05 16:07:54 -06:00
Carl Meyer
af12ee1763 Switch from colored to owo_colors/anstream 2024-04-04 17:55:43 -06:00
Alex Waygood
6b4fa17097 Rework docs for pydocstyle rules (#10754) 2024-04-03 22:34:00 +01:00
Carl Meyer
5e2482824c [flake8_comprehensions] add sum/min/max to unnecessary comprehension check (C419) (#10759)
Fixes #3259 

## Summary

Renames `UnnecessaryComprehensionAnyAll` to
`UnnecessaryComprehensionInCall` and extends the check to `sum`, `min`,
and `max`, in addition to `any` and `all`.

## Test Plan

Updated snapshot test.

Built docs locally and verified the docs for this rule still render
correctly.
2024-04-03 14:44:33 -06:00
Carl Meyer
e0a8fb607a fix obsolete name in resolve_qualified_name docs (#10762)
`resolve_call_path` was renamed to `resolve_qualified_name` in
a6d892b1f4, but the doc block for the
function wasn't updated to match.
2024-04-03 20:13:10 +00:00
Jane Lewis
257964a8bc ruff server now supports source.fixAll source action (#10597)
## Summary

`ruff server` now has source action `source.fixAll` as an available code
action.

This also fixes https://github.com/astral-sh/ruff/issues/10593 in the
process of revising the code for quick fix code actions.

## Test Plan




https://github.com/astral-sh/ruff/assets/19577865/f4c07425-e68a-445f-a4ed-949c9197a6be
2024-04-03 16:22:17 +00:00
Boshen
d467aa78c2 Remove an unused dependency (#10747)
## Summary

Continuation of #10475, I improved [`cargo
shear`](https://github.com/Boshen/cargo-shear) even more.

We can put this in CI once I test it a bit more, given that [ignoring
false
positives](https://github.com/Boshen/cargo-shear?tab=readme-ov-file#ignore-false-positives)
has been implemented.

## Test Plan

`cargo check --all-features --all-targets`
2024-04-03 09:57:19 +01:00
renovate[bot]
6e1c061e5f chore(deps): update rust crate lsp-types to v0.95.1 (#10686)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [lsp-types](https://togithub.com/gluon-lang/lsp-types) |
workspace.dependencies | patch | `0.95.0` -> `0.95.1` |

---

### Release Notes

<details>
<summary>gluon-lang/lsp-types (lsp-types)</summary>

###
[`v0.95.1`](https://togithub.com/gluon-lang/lsp-types/blob/HEAD/CHANGELOG.md#v0951-2024-03-18)

[Compare
Source](https://togithub.com/gluon-lang/lsp-types/compare/v0.95.0...v0.95.1)

##### v0.95.1 (2024-03-18)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "on Monday" (UTC), Automerge - At any
time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 06:37:03 +00:00
Jane Lewis
9872f51293 Drop support for root_uri as an initialization parameter in ruff_server (#10743)
## Summary

Needed for https://github.com/astral-sh/ruff/pull/10686.

We no longer support `root_uri` as an initialization parameter, relying
solely on `workspace_folders` to find the working directories. This
means that the minimum supported LSP version is now `0.3.6`.

## Test Plan

When opening a folder in VS Code, you shouldn't see any errors in the
log which say `No workspace(s) were provided(...)`.
2024-04-02 20:51:59 -07:00
Charlie Marsh
e54b591ec7 Use section name range for all section-related docstring diagnostics (#10740)
## Summary

We may not have had access to this in the past, but in short, if the
diagnostic is related to a specific section of a docstring, it seems
better to highlight the section (via the header) than the _entire_
docstring.

This should be completely compatible with existing `# noqa` since it's
always inside of a multi-line string anyway, and in such cases the `#
noqa` is always placed at the end of the multiline string.

Closes https://github.com/astral-sh/ruff/issues/10736.
2024-04-02 22:10:04 -04:00
Carl Meyer
814b26f82e [flake8_comprehensions] update docs for unnecessary-comprehension-any-all (C419) (#10744)
Ref #3259; see in particular
https://github.com/astral-sh/ruff/issues/3259#issuecomment-2033127339

## Summary

Improve the accuracy of the docs for this lint rule/fix.

## Test Plan

Generated the docs locally and visited the page for this rule:

![Screenshot 2024-04-02 at 4 56
40 PM](https://github.com/astral-sh/ruff/assets/61586/64f25cf6-edfe-4682-ac8e-7e21b834f5f2)

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-04-02 17:42:55 -06:00
Carl Meyer
2a4084a2bb chore(docs): update ruff_linter crate name in CONTRIBUTING.md (#10745)
Reading through `CONTRIBUTING.md`, I happened to notice that it still
referred to the `ruff_linter` crate as the `ruff` crate. `ruff` is a
different crate, located in `crates/ruff`, which doesn't contain "the
vast majority of the code and all the lint rules."
2024-04-02 17:06:21 -06:00
Charlie Marsh
dff8f93457 [flake8-return] Ignore assignments to annotated variables in unnecessary-assign (#10741)
## Summary

Closes https://github.com/astral-sh/ruff/issues/10732.
2024-04-02 16:18:05 -04:00
Aleksei Latyshev
859e3fc7fa [refurb] Implement int-on-sliced-str (FURB166) (#10650)
## Summary
implement int_on_sliced_str (FURB166) lint
- #1348
- [original
lint](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/use_int_base_zero.py)

## Test Plan
cargo test
2024-04-02 19:29:42 +00:00
Aleksei Latyshev
0de23760ff [pylint] Don't recommend decorating staticmethods with @singledispatch (PLE1519, PLE1520) (#10637)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-04-02 16:47:31 +00:00
renovate[bot]
b90e6df5cc chore(deps): update rust crate env_logger to 0.11.0 (#10700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 15:03:54 +02:00
Micha Reiser
0d20ec968f Build codspeed benchmarks by calling cargo directly (#10735) 2024-04-02 14:50:19 +02:00
Dhruv Manilawala
99dd3a8ab0 Implement as_str & Display for all operator enums (#10691)
## Summary

This PR adds the `as_str` implementation for all the operator methods.
It already exists for `CmpOp` which is being [used in the
linter](ffcd77860c/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs (L117))
and it makes sense to implement it for the rest as well. This will also
be utilized in error messages for the new parser.
2024-04-02 10:34:36 +00:00
Dhruv Manilawala
eee2d5b915 Remove unused operator methods and impl (#10690)
## Summary

This PR removes unused operator methods and impl traits. There is
already the `is_macro::Is` implementation for all the operators and this
seems unnecessary.
2024-04-02 15:53:20 +05:30
Charlie Marsh
159bad73d5 Accept non-aliased (but correct) import in unconventional-import-alias (#10729)
## Summary

Given:

```toml
[tool.ruff.lint.flake8-import-conventions.aliases]
"django.conf.settings" = "settings"
```

We should accept `from django.conf import settings`.

Closes https://github.com/astral-sh/ruff/issues/10599.
2024-04-01 23:47:20 -04:00
Charlie Marsh
7b48443624 Respect Q00* ignores in flake8-quotes rules (#10728)
## Summary

We lost the per-rule ignores when these were migrated to the AST, so if
_any_ `Q` rule is enabled, they're now all enabled.

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

## Test Plan

Ran:

```shell
ruff check . --isolated --select Q --ignore Q000
ruff check . --isolated --select Q --ignore Q001
ruff check . --isolated --select Q --ignore Q002
ruff check . --isolated --select Q --ignore Q000,Q001
ruff check . --isolated --select Q --ignore Q000,Q002
ruff check . --isolated --select Q --ignore Q001,Q002
```

...against:

```python
'''
bad docsting
'''
a = 'single'
b = '''
bad multi line
'''
```
2024-04-02 03:21:12 +00:00
Charlie Marsh
d36f60999d Ignore annotated lambdas in class scopes (#10720)
## Summary

An annotated lambda assignment within a class scope is often
intentional. For example, within a dataclass or Pydantic model, these
are treated as fields rather than methods (and so can be passed values
in constructors).

I originally wrote this to special-case dataclasses and Pydantic
models... But was left feeling like we'd see more false positives here
for little gain (an annotated lambda within a `class` is likely
intentional?). Open to opinions, though.

Closes https://github.com/astral-sh/ruff/issues/10718.
2024-04-01 15:44:45 -04:00
Charlie Marsh
67f0f615b2 Recursively resolve TypeDicts for N815 violations (#10719)
## Summary

Only works within a single file for now.

Closes https://github.com/astral-sh/ruff/issues/10671.
2024-04-01 17:40:55 +00:00
Charlie Marsh
200ebeebdc Bump version to v0.3.5 (#10717) 2024-04-01 13:15:32 -04:00
renovate[bot]
23e8279093 chore(deps): update npm development dependencies (#10716) 2024-04-01 16:10:33 +00:00
renovate[bot]
221b3236a8 chore(deps): update strum to 0.26.0 (#10715) 2024-04-01 16:06:51 +00:00
renovate[bot]
a0e1544848 chore(deps): update rust crate pep440_rs to 0.5.0 (#10703) 2024-04-01 16:02:18 +00:00
Alex Waygood
2740fab7ad Renovate: group all strum dependencies together (#10714) 2024-04-01 15:57:07 +00:00
renovate[bot]
7042b9b16d fix(deps): update rust crate similar to v2.5.0 (#10711) 2024-04-01 11:54:58 -04:00
renovate[bot]
4047d456b6 chore(deps): update rust crate insta to v1.38.0 (#10701) 2024-04-01 15:44:30 +00:00
renovate[bot]
20d69ea504 chore(deps): update npm development dependencies (#10697) 2024-04-01 10:45:07 -04:00
renovate[bot]
d021cac0c9 chore(deps): update rust crate tracing-tree to 0.3.0 (#10709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-01 14:21:50 +01:00
renovate[bot]
46369d48fe chore(deps): update rust crate uuid to v1.8.0 (#10710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-01 14:20:05 +01:00
renovate[bot]
85d59198aa chore(deps): update tj-actions/changed-files action to v44 (#10712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-01 14:19:28 +01:00
renovate[bot]
20a2e25cb0 chore(deps): update rust crate serde_with to v3.7.0 (#10706) 2024-04-01 12:37:41 +00:00
renovate[bot]
a32e70d449 chore(deps): update rust crate insta-cmd to 0.5.0 (#10702) 2024-04-01 11:55:05 +00:00
renovate[bot]
76d0edbbaa chore(deps): update rust crate toml to v0.8.12 (#10696) 2024-04-01 11:53:43 +00:00
Alex Waygood
8d547ef83a Allow renovate to create more PRs at once (#10695) 2024-04-01 11:43:07 +00:00
renovate[bot]
d7a6978e05 chore(deps): update rust crate syn to v2.0.57 (#10694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-01 11:24:48 +00:00
renovate[bot]
786ff403b1 chore(deps): update rust crate serde_json to v1.0.115 (#10693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-01 11:24:15 +00:00
Alex Waygood
2ea0c3dce6 Give renovate more time in which to file PRs (#10692) 2024-04-01 11:13:14 +00:00
renovate[bot]
b9dfa7845f chore(deps): update rust crate memchr to v2.7.2 (#10688) 2024-04-01 08:25:05 +01:00
renovate[bot]
090f6580d3 Update rust crate regex to v1.10.4 (#10689) 2024-04-01 08:24:13 +01:00
renovate[bot]
eb884c8f76 chore(deps): update rust crate indoc to v2.0.5 (#10685) 2024-04-01 01:10:27 +00:00
Steve C
f6b6f0df67 [ruff] Fix panic in unused # noqa removal with multi-byte space (RUF100) (#10682)
## Summary

Currently, [this
line](716688d44e/crates/ruff_linter/src/fix/edits.rs (L101))
assumes that the `noqa` comment begins with an octothorpe followed by a
space. (`# `) With anyone's random code, this of course is not always
true.

When there's a multi-byte character after the leading octothorpe, such
as
[`\u0085`](https://www.fileformat.info/info/unicode/char/85/index.htm),
we try slicing from within the character, causing a panic.

To fix this, the logic has been changed to remove unused `noqa`
directives and keep any trailing comments, or removing the whole comment
if the comment is just the unused `noqa`

Fixes #10097.

## Test Plan

`cargo test`
2024-04-01 01:00:37 +00:00
renovate[bot]
9f2127bf04 Update rust crate clap to v4.5.4 (#10684) 2024-04-01 01:16:02 +01:00
renovate[bot]
8cd096d9b5 chore(deps): update rust crate chrono to v0.4.37 (#10683) 2024-04-01 01:15:24 +01:00
hikaru-kajita
716688d44e [pylint] Implement modified-iterating-set (E4703) (#10473)
## Summary

Implement `E4703` in the issue #970.
Relevant pylint docs is here:
https://pylint.readthedocs.io/en/stable/user_guide/messages/error/modified-iterating-set.html

## Test Plan

I've written it in the `modified_iterating_set.py`.
2024-03-31 14:37:49 +00:00
hikaru-kajita
9ad9cea952 [refurb] Implement unnecessary-from-float (FURB164) (#10647)
## Summary

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

Implement FURB164 in the issue #1348.
Relevant Refurb docs is here:
https://github.com/dosisod/refurb/blob/v2.0.0/docs/checks.md#furb164-no-from-float

I've changed the name from `no-from-float` to
`verbose-decimal-fraction-construction`.

## Test Plan

<!-- How was it tested? -->

I've written it in the `FURB164.py`.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-03-30 07:04:01 -04:00
Charlie Marsh
75e01420fa Always place non-relative imports after relative imports (#10669)
## Summary

When `relative-imports-order = "closest-to-furthest"` is set, we should
_still_ put non-relative imports after relative imports. It's rare for
them to be in the same section, but _possible_ if you use
`known-local-folder`.

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

## Test Plan

New tests.

Also sorted this file:

```python
from ..models import ABC
from .models import Question
from .utils import create_question
from django_polls.apps.polls.models import Choice
```

With both:

- `isort view.py`
- `ruff check view.py --select I --fix`

And the following `pyproject.toml`:

```toml
[tool.ruff.lint.isort]
order-by-type = false
relative-imports-order = "closest-to-furthest"
known-local-folder = ["django_polls"]

[tool.isort]
profile = "black"
reverse_relative = true
known_local_folder = ["django_polls"]
```

I verified that Ruff and isort gave the same result, and that they
_still_ gave the same result when removing the relevant setting:

```toml
[tool.ruff.lint.isort]
order-by-type = false
known-local-folder = ["django_polls"]

[tool.isort]
profile = "black"
known_local_folder = ["django_polls"]
```
2024-03-29 22:13:54 -04:00
Charlie Marsh
fc54f53662 Make unnecessary-lambda an always-unsafe fix (#10668)
## Summary

See the linked issue and comment for more.

Closes https://github.com/astral-sh/ruff/issues/10663.
2024-03-30 01:05:05 +00:00
Charlie Marsh
7c2e9f71ea Change quadratic-list-summation docs to use iadd consistently (#10666)
Closes https://github.com/astral-sh/ruff/issues/10088.
2024-03-30 00:32:34 +00:00
Auguste Lalande
3c48913473 [flake8-boolean-trap] Add setting for user defined allowed boolean trap (#10531)
## Summary

Add a setting `extend-allowed-calls` to allow users to define their own
list of calls which allow boolean traps.

Resolves #10485.
Resolves #10356.

## Test Plan

Extended text fixture and added setting test.
2024-03-30 00:26:12 +00:00
Charlie Marsh
9f56902719 Add PR title format to CONTRIBUTING.md (#10665)
Closes https://github.com/astral-sh/ruff/issues/9470.
2024-03-30 00:14:16 +00:00
Dhruv Manilawala
1bcdfe268d Allow f-strings with %z for DTZ007 (#10651)
## Summary

This PR fixes the bug for `DTZ007` rule where it didn't consider to
check for the presence of `%z` in f-strings. It also considers the
string parts of an implicitly concatenated f-strings for which I want to
find a better solution (#10308).

fixes: #10601 

## Test Plan

Add test cases and update the snapshots.
2024-03-29 09:57:13 +05:30
Mateusz Sokół
a0263ab472 Add row_stack to NumPy 2.0 migration rule (#10646)
Hi! 

I left out one of the functions in the migration rule which became
deprecated/removed in NumPy 2.0
(https://github.com/numpy/numpy/issues/26032#issuecomment-1999870797).

cc @anntzer
2024-03-28 09:49:40 -04:00
Jane Lewis
6b580c1544 Support unused code formatting for ruff server (#10644)
## Summary

Fixes #10589.

Code that violates `F401` or `F841` (in other words, unused variables or
imports) should now appear greyed out or 'unused' in an editor.

## Test Plan

Put the following test code in a new file within the extension
development host window:

```python
import math
def func():
   if False:
      unused = "<- this should be greyed out"
```

The following test code should have greyed out/unused import and
variable names, like so:
<img width="294" alt="Screenshot 2024-03-28 at 4 23 18 AM"
src="https://github.com/astral-sh/ruff/assets/19577865/e84a6e7a-49e2-4fed-9624-f8f9559e0837">
2024-03-28 11:30:35 +00:00
Jane Lewis
3f7d666e8b ruff server now highlights most issues as warnings (#10643)
## Summary

Fixes #10588.

Most diagnostics from `ruff server` now appear as a much less alarming
warning instead of an error. Three diagnostics still appear as errors:
`F821` (`undefined name <name>`), `E902` (`IOError`) and `E999`
(`SyntaxError`).

## Test Plan

With an extension using the path to a locally-built executable, open a
file with multiple highlighted problems. Toggle the `Experimental
Server` setting on and off. The highlights should stay as warnings.

Then, modify the file to have a syntactically incorrect element. The
start of the invalid syntax should now have a red highlight.
2024-03-28 04:14:17 -07:00
Mix
cce25ec116 Update warning message for rule S305 to address insecure block cipher mode use (#10602)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

This PR updates the warning message for rule S305 to accurately reflect
the security concern over using ECB mode in block ciphers, which is
considered insecure compared to other modes like CBC or CTR. The
previous message incorrectly mentioned AES as a [block cipher
mode](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation),
which has been corrected to avoid confusion.

Ref: 

c85576d903/bandit/blacklists/calls.py (L99-L102)


825fd7c990/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs (L187-L216)

## Test Plan

No testing required as the change is limited to a minor change of
warning message update.
2024-03-27 21:00:49 -04:00
Peter A. Jonsson
fc7fa59e5f space_around_operator: use same before/after numbers (#10640)
## Summary

The example for tab-after-comma (E242):
```python
a = 4,\t5
```
Use instead:
```python
a = 4, 3
```
is confusing since both the whitespace and the numbers are changed.

Change so the examples use the same numbers before/after.

## Test Plan

Untested.
2024-03-27 19:31:19 -04:00
Alex Waygood
abbefae6f1 DTZ rules: Clarify error messages and docs (#10621)
- Clearly state in the documentation that passing `tz=None` is just as bad as not passing a `tz=` argument, from the perspective of these rules.
- Clearly state in the error messages exactly what the user is doing wrong, if the user is passing `tz=None` rather than failing to pass a `tz=` argument at all.
- Make error messages more concise, and separate out the suggested remedy from the thing that the user is identified as doing wrong.

Co-authored-by: Christian Clauss <cclauss@me.com>
2024-03-27 19:42:13 +00:00
Aleksei Latyshev
f9d0c6d9ae [refurb] Implement for-loop-set-mutations (FURB142) (#10583)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-03-27 09:26:12 +01:00
Jane Lewis
4d59142255 Client request sender and inbound response handling for ruff server (#10620)
## Summary

Fixes #10618.

This PR introduces a proper API for sending requests to the client and
handling any response sent back. Dynamic capability registration now
uses this new API, fixing an issue where a much more simplistic response
handler silently flushes a code action request that needed a response.

## Test Plan

#10618 can no longer be reproduced. No errors about unhandled responses
should appear in the extension output, and you should see this new log
when the server starts:
```
<DATE> <TIME> [info] <DURATION> INFO ruff_server::server Configuration file watcher successfully registered
```
2024-03-26 13:53:56 -07:00
renovate[bot]
72aa1ce00f Update dependency uuid to v9.0.1 (#10615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 12:05:17 +00:00
renovate[bot]
b6b737c937 Update dependency @miniflare/storage-memory to v2.14.2 (#10614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 12:04:59 +00:00
renovate[bot]
b074e7dc9b Update dependency @miniflare/kv to v2.14.2 (#10613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 12:03:41 +00:00
renovate[bot]
e81f1f7971 Update NPM Development dependencies (#10610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 12:53:29 +01:00
renovate[bot]
83c3580346 Update dependency react-resizable-panels to v2 (#10611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-03-26 11:52:25 +00:00
renovate[bot]
f0662eea48 Update Monaco (#10609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-03-26 11:41:21 +00:00
Micha Reiser
b49b861b2d Delete unused Violation::explanation method (#10600) 2024-03-26 12:35:27 +01:00
Micha Reiser
877a9145ae Group some NPM dependency updates (#10607) 2024-03-26 11:39:56 +01:00
renovate[bot]
ba24bd88cf Update dependency classnames to v2.5.1 (#10603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 09:01:35 +00:00
Jane Lewis
825fd7c990 High-level project overview and contributing guide for ruff server (#10565)
## Summary

This PR adds an overview and roadmap to the `README.md` for the
`ruff_server` crate along with a rudimentary `CONTRIBUTING.md` that
explains some of the technical decisions behind the project and basic
information about local testing.
2024-03-25 23:08:37 -07:00
hikaru-kajita
a28776e3aa [flake8-comprehensions] Handled special case for C401 which also matches C416 (#10596)
## Summary

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

Similar to #10419, there was a case where there is a collision of C401
and C416 (as discussed in #10101).
Fixed this by implementing short-circuit for the comprehension of the
form `{x for x in foo}`.

## Test Plan

<!-- How was it tested? -->

Extended `C401.py` with the case where `set` is not builtin function,
and divided the case where the short-circuit should occur.
Removed the last testcase of `print(f"{ {set(a for a in 'abc')} }")`
test as this is invalid as a python code, but should I keep this?
2024-03-26 03:54:58 +00:00
Aurelio Jargas
80b46889ed Fix list markup: blank line required (#10591)
Judging from the other lists in this same file, it looks like a blank
line is required to separate the previous paragraph from the first list
item.

Otherwise the list is considered part of the paragraph:

- https://docs.astral.sh/ruff/settings/#lint_flake8-copyright_notice-rgx

<img width="400" alt="image"
src="https://github.com/astral-sh/ruff/assets/282592/663fa5b4-f787-4fcd-bdd6-0b891d7ad72b">

- https://docs.astral.sh/ruff/settings/#format_quote-style

<img width="400" alt="image"
src="https://github.com/astral-sh/ruff/assets/282592/809735eb-c546-4348-9311-877441bae84e">

After this change hopefully those will be rendered as proper bullet
lists.
2024-03-26 00:18:35 +00:00
renovate[bot]
fdd25f0d99 Update dependency vite to v4.5.2 (#10576) 2024-03-25 17:50:41 -05:00
renovate[bot]
e0ab5629cc Update dependency wrangler to v2.20.2 (#10577) 2024-03-25 17:50:23 -05:00
Filipe Laíns
960e47423c Put flake8-logging next to the other flake8 plugins in registry (#10587)
## Summary

This is just a nitpicky improvement, but I thought it'd be a good
opportunity to look at the ruff source.

> The rules list in the documentation is generated using the registry
order. Currently, flake8-logging is separated from the rest of the
flake8 plugins. This patch puts it next to them.

https://docs.astral.sh/ruff/rules/

If it makes sense, we could alternatively just sort the linters in
https://github.com/astral-sh/ruff/blob/main/crates/ruff_dev/src/generate_rules_table.rs.

Signed-off-by: Filipe Laíns <lains@riseup.net>
2024-03-25 20:23:31 +00:00
Dhruv Manilawala
4950ca4142 Ignore Q000, Q001 when string is inside forward ref (#10585)
## Summary

This is not the holistic solution but just to fix that issue.

fixes: #10546 

## Test Plan

Add a regression test for it and check the snapshots.
2024-03-25 18:52:59 +00:00
renovate[bot]
3e1f3b8132 Update Rust crate anyhow to v1.0.81 (#10584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 18:28:02 +00:00
Alex Waygood
21f63c57d5 Renovate: Use update-lockfile as the rangeStrategy for cargo dependencies (#10582) 2024-03-25 18:17:03 +00:00
Alex Waygood
bd07c13348 Get rid of SCARY SECURITY suffixes for security-related renovate PRs (#10579) 2024-03-25 17:57:12 +00:00
Alex Waygood
9e21e5918c Switch from dependabot to renovate (#10567) 2024-03-25 17:30:11 +00:00
hikaru-kajita
f7aab5ac69 [pylint] Fixed false-positive on the rule PLW1641 (eq-without-hash) (#10566)
## Summary

Fixed false-positive on the rule `PLW1641`, where the explicit
assignment on the `__hash__` method is not counted as an definition of
`__hash__`. (Discussed in #10557).

Also, added one new testcase.

## Test Plan

Checked on `cargo test` in `eq_without_hash.py`.

Before the change, for the assignment into `__hash__`, only `__hash__ =
None` was counted as an explicit definition of `__hash__` method.
Probably any assignment into `__hash__` property could be counted as an
explicit definition of hash, so I removed `value.is_none_literal_expr()`
check.
2024-03-25 14:40:01 +00:00
Alex Waygood
59ac3f48c8 Select more rules for typeshed in the ecosystem checks (#10564) 2024-03-25 14:04:11 +00:00
Hoël Bagard
9512bd66b5 [pycodestyle] Avoid blank line rules for the first logical line in cell (#10291)
## Summary

Closes #10228

The PR makes the blank lines rules keep track of the cell status when
running on a notebook, and makes the rules not trigger when the line is
the first of the cell.

## Test Plan

The example given in #10228 is added as a fixture, along with a few
tests from the main blank lines fixtures.
2024-03-25 11:19:30 +00:00
dependabot[bot]
5729dc3589 Bump syn from 2.0.52 to 2.0.55 (#10563)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 10:49:48 +01:00
dependabot[bot]
2890485785 Bump aho-corasick from 1.1.2 to 1.1.3 (#10560)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 09:27:07 +00:00
dependabot[bot]
a5f6e5dc88 Bump rayon from 1.9.0 to 1.10.0 (#10559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 10:16:52 +01:00
dependabot[bot]
d93db63a22 Bump smallvec from 1.13.1 to 1.13.2 (#10562)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 10:16:08 +01:00
dependabot[bot]
ecc11dcc12 Bump bitflags from 2.4.2 to 2.5.0 (#10561)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 10:14:39 +01:00
Dhruv Manilawala
e9115b8d8a Move Q004 to AST based checker (#10548)
## Summary

Continuing with #7595, this PR moves the `Q004` rule to the AST checker.

## Test Plan

- [x] Existing test cases should pass
- [x] No ecosystem updates
2024-03-25 03:31:31 +00:00
Alexey Preobrazhenskiy
d625f55c05 Nested namespace packages support (#10541)
## Summary
PEP 420 says [nested namespace
packages](https://peps.python.org/pep-0420/#nested-namespace-packages)
are allowed, i.e. marking a directory as a namespace package marks all
subdirectories in the subtree as namespace packages.

`is_package` is modified to use `Path::starts_with` and the order of
checks is reversed to do in-memory checks first before hitting the disk.

## Test Plan
Added unit tests. Previously all tests were run with `namespace_packages
== &[]`. Verified that one of the tests was failing before changing the
implementation.

## Future Improvements
The `is_package_with_cache` can probably be rewritten to avoid repeated
calls to `Path::starts_with`, by caching all directories up to the
`namespace_root`:
```ruff
let namespace_root = namespace_packages
    .iter()
    .filter(|namespace_package| path.starts_with(namespace_package))
    .min();
```
2024-03-24 22:53:32 -04:00
Charlie Marsh
9856c1446b Document use of anonymous assignment in useless-expression (#10551)
Closes https://github.com/astral-sh/ruff/issues/10536.
2024-03-25 02:46:33 +00:00
hikaru-kajita
39fb6d9bfc [refurb] Implement verbose-decimal-constructor (FURB157) (#10533)
## Summary

Implement FURB157 in the issue #1348.
Relevant Refurb docs is here:
https://github.com/dosisod/refurb/blob/master/docs/checks.md#furb157-simplify-decimal-ctor

## Test Plan

I've written it in the `FURB157.py`.
2024-03-24 22:28:58 -04:00
yt2b
22f237fec6 [flake8-bugbear] Avoid false positive for usage after continue (B031) (#10539)
## Summary

Closes #10337.

I've fixed the code to count usage of variable.
Usage count inside the block is reset when there is a following
statement.
- continue
- break
- return 

## Test Plan

Add test case.
2024-03-25 00:38:30 +00:00
Alex Waygood
021f0bdccb Mark PYI025 fix as safe in more cases for stub files (#10547)
## Summary

The fix for PYI025 is currently marked as unsafe in non-global scopes
for both `.py` and `.pyi` files, on the grounds that all global-scope
symbols in Python are implicitly exported from the module, so changing
the name of something in the global scope could break other modules that
import the module we're fixing. Unlike in `.py` files, however, imported
symbols are never implicitly re-exported from stub files. Symbols are
only understood by static analysis tools as being re-exported from stubs
if they are marked as explicit re-exports, which take three forms:

```py
from foo import *  # all symbols from foo are re-exported from the stub

# the "redundant" alias marks it as an explicit re-export
# (note that the alias needs to be identical to the symbol's "actual" name
# in order for it to be a re-export)
from bar import barrr as barrr

# inclusion in __all__ also marks it as an explicit re-export,
# just like in `.py` files
from baz import bazzz
__all__ = ["bazzz"]
```

This is [specc'd in PEP
484](https://peps.python.org/pep-0484/#stub-files), and means that we
can mark the fix for PYI025 as safe in more cases for `.pyi` files.

## Test Plan

`cargo test`. An existing test case goes from being an unsafe fix to a
safe fix in a `.pyi` fixture. I also added a new fixture so we have
coverage of global-scope imports that are marked as re-exports using
"redundant" `from collections.abc import Set as Set` aliases.
2024-03-24 16:11:48 +00:00
Alexey Preobrazhenskiy
7cc40d5621 Refactor package_roots for readability. (#10543) 2024-03-24 12:02:30 +00:00
Dhruv Manilawala
c447454111 [E402] Allow cell magics before an import (#10545) 2024-03-24 16:20:00 +05:30
Dhruv Manilawala
895d9df02f Move Q001-3 to AST based checker (#10312)
## Summary

Continuing with https://github.com/astral-sh/ruff/issues/7595, this PR
moves the `Q001`, `Q002`, `Q003` rules to the AST based checker.

## Test Plan

Make sure all of the existing test cases pass and verify there are no
ecosystem changes.
2024-03-23 22:59:50 +05:30
Auguste Lalande
0c194f55e8 Fix PT014 autofix for last item in list (#10532)
## Summary

This error was found browsing
https://github.com/qarmin/Automated-Fuzzer/actions/runs/8396966850.
Which failed when trying to autofix the PT014 violation in the following
code:
```python
@pytest.mark.parametrize('data, spec', [(1.0, 1.0), (1.0, 1.0)])
def test_numbers(data, spec):
    ...
```

Investigation revealed that the implementation was not properly tested,
when the duplicate value was also the last in the list. In particular
the following function, which is in charge of finding the comma
following an element to create the suggested fix,

0a99bd84ce/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs (L647-L651)
would find the next comma even if it was outside the list itself leading
to a lot of code being deleted.

This PR fixes that.

## Test Plan

Added misbehaving code to the test fixture.
2024-03-23 09:26:42 -04:00
Zanie Blue
0a99bd84ce Add PlasmaPy to ecosystem checks (#10530)
ref
https://github.com/astral-sh/ruff/issues/10528#issuecomment-2015805710
2024-03-22 15:28:13 -05:00
Alex Waygood
9feb9b0aa8 Correctly handle references in __all__ definitions when renaming symbols in autofixes (#10527) 2024-03-22 20:06:35 +00:00
Charlie Marsh
61b7982422 Respect Unicode characters in import sorting (#10529)
## Summary

Ensures that we use the raw identifier as provided in the source code,
rather than the normalized Unicode identifier.

This _does_ mean that we treat these as two separate identifiers, and
_don't_ merge them, even though Python will treat them as the same
symbol:

```python
import numpy as ℂℇℊℋℌℍℎℐℑℒℓℕℤΩℨKÅℬℭℯℰℱℹℴ
import numpy as CƐgHHHhIILlNZΩZKÅBCeEFio
```

I think that's fine, this is super rare anyway and would likely be
confusing for users.

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

## Test Plan

`cargo test`
2024-03-22 15:16:49 -04:00
Ryan May
594b232e0f Accept commas in default copyright pattern (#9498)
## Summary

Adds commas as an accepted separator between copyright years by default,
which is actually documented in one spot, but not currently accurate.
Fixes #9477.
2024-03-22 14:42:02 -04:00
Alex Waygood
a06ffeb54e Track ranges of names inside __all__ definitions (#10525) 2024-03-22 18:38:40 +00:00
Alex Waygood
b74dd420fc Fix F821 false negatives when from __future__ import annotations is active (attempt 2) (#10524) 2024-03-22 18:11:16 +00:00
Jane Lewis
4f06d59ff6 Automatic configuration reloading for ruff server (#10404)
## Summary

Fixes #10366.

`ruff server` now registers a file watcher on the client side using the
LSP protocol, and listen for events on configuration files. On such an
event, it reloads the configuration in the 'nearest' workspace to the
file that was changed.

## Test Plan

N/A
2024-03-21 20:17:07 +00:00
285 changed files with 9359 additions and 6141 deletions

View File

@@ -1,21 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels: ["internal"]
groups:
actions:
patterns:
- "*"
ignore:
# The latest versions of these are not compatible with our release workflow
- dependency-name: "actions/upload-artifact"
- dependency-name: "actions/download-artifact"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
labels: ["internal"]

60
.github/renovate.json5 vendored Normal file
View File

@@ -0,0 +1,60 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
dependencyDashboard: true,
suppressNotifications: ["prEditedNotification"],
extends: ["config:recommended"],
labels: ["internal"],
schedule: ["on Monday"],
separateMajorMinor: false,
prHourlyLimit: 10,
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "npm"],
cargo: {
// See https://docs.renovatebot.com/configuration-options/#rangestrategy
rangeStrategy: "update-lockfile",
},
pep621: {
fileMatch: ["^(python|scripts)/.*pyproject\\.toml$"],
},
npm: {
fileMatch: ["^playground/.*package\\.json$"],
},
"pre-commit": {
enabled: true,
},
packageRules: [
{
// Group upload/download artifact updates, the versions are dependent
groupName: "Artifact GitHub Actions dependencies",
matchManagers: ["github-actions"],
matchPackagePatterns: ["actions/.*-artifact"],
description: "Weekly update of artifact-related GitHub Actions dependencies",
},
{
groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"],
description: "Weekly update of pre-commit dependencies",
},
{
groupName: "NPM Development dependencies",
matchManagers: ["npm"],
matchDepTypes: ["devDependencies"],
description: "Weekly update of NPM development dependencies",
},
{
groupName: "Monaco",
matchManagers: ["npm"],
matchPackagePatterns: ["monaco"],
description: "Weekly update of the Monaco editor",
},
{
groupName: "strum",
matchManagers: ["cargo"],
matchPackagePatterns: ["strum"],
description: "Weekly update of strum dependencies",
},
],
vulnerabilityAlerts: {
commitMessageSuffix: "",
labels: ["internal", "security"],
},
}

View File

@@ -35,7 +35,7 @@ jobs:
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v43
- uses: tj-actions/changed-files@v44
id: changed
with:
files_yaml: |
@@ -525,8 +525,23 @@ jobs:
- uses: Swatinem/rust-cache@v2
# Codspeed comes with a very ancient cargo version (1.66) that resolves features flags differently than what we use now.
# This can result in build failures; see https://github.com/astral-sh/ruff/pull/10700.
# There's a pending codspeed PR to upgrade to a newer cargo version, but until that's merged, we need to use the workaround below.
# https://github.com/CodSpeedHQ/codspeed-rust/pull/31
# What we do is to call cargo build manually with the correct feature flags and RUSTC settings. We'll have to
# manually maintain the list of benchmarks to run with codspeed (the benefit is that we could detect which benchmarks to run and build based on the changes).
# This is inspired by https://github.com/oxc-project/oxc/blob/a0532adc654039a0c7ead7b35216dfa0b0cb8e8f/.github/workflows/benchmark.yml
- name: "Build benchmarks"
run: cargo codspeed build --features codspeed -p ruff_benchmark
env:
RUSTFLAGS: "-C debuginfo=2 -C strip=none -g --cfg codspeed"
shell: bash
# Build all benchmarks, copy the binary to the codspeed directory, remove any `*.d` files that might have been created.
run: |
cargo build --release -p ruff_benchmark --bench parser --bench linter --bench formatter --bench lexer --features=codspeed
mkdir -p ./target/codspeed/ruff_benchmark
cp ./target/release/deps/{lexer,parser,linter,formatter}* target/codspeed/ruff_benchmark/
rm -rf ./target/codspeed/ruff_benchmark/*.d
- name: "Run benchmarks"
uses: CodSpeedHQ/action@v2

View File

@@ -1,5 +1,54 @@
# Changelog
## 0.3.5
### Preview features
- \[`pylint`\] Implement `modified-iterating-set` (`E4703`) ([#10473](https://github.com/astral-sh/ruff/pull/10473))
- \[`refurb`\] Implement `for-loop-set-mutations` (`FURB142`) ([#10583](https://github.com/astral-sh/ruff/pull/10583))
- \[`refurb`\] Implement `unnecessary-from-float` (`FURB164`) ([#10647](https://github.com/astral-sh/ruff/pull/10647))
- \[`refurb`\] Implement `verbose-decimal-constructor` (`FURB157`) ([#10533](https://github.com/astral-sh/ruff/pull/10533))
### Rule changes
- \[`flake8-comprehensions`\] Handled special case for `C401` which also matches `C416` ([#10596](https://github.com/astral-sh/ruff/pull/10596))
- \[`flake8-pyi`\] Mark `unaliased-collections-abc-set-import` fix as "safe" for more cases in stub files (`PYI025`) ([#10547](https://github.com/astral-sh/ruff/pull/10547))
- \[`numpy`\] Add `row_stack` to NumPy 2.0 migration rule ([#10646](https://github.com/astral-sh/ruff/pull/10646))
- \[`pycodestyle`\] Allow cell magics before an import (`E402`) ([#10545](https://github.com/astral-sh/ruff/pull/10545))
- \[`pycodestyle`\] Avoid blank line rules for the first logical line in cell ([#10291](https://github.com/astral-sh/ruff/pull/10291))
### Configuration
- Respected nested namespace packages ([#10541](https://github.com/astral-sh/ruff/pull/10541))
- \[`flake8-boolean-trap`\] Add setting for user defined allowed boolean trap ([#10531](https://github.com/astral-sh/ruff/pull/10531))
### Bug fixes
- Correctly handle references in `__all__` definitions when renaming symbols in autofixes ([#10527](https://github.com/astral-sh/ruff/pull/10527))
- Track ranges of names inside `__all__` definitions ([#10525](https://github.com/astral-sh/ruff/pull/10525))
- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`) ([#10539](https://github.com/astral-sh/ruff/pull/10539))
- \[`flake8-copyright`\] Accept commas in default copyright pattern ([#9498](https://github.com/astral-sh/ruff/pull/9498))
- \[`flake8-datetimez`\] Allow f-strings with `%z` for `DTZ007` ([#10651](https://github.com/astral-sh/ruff/pull/10651))
- \[`flake8-pytest-style`\] Fix `PT014` autofix for last item in list ([#10532](https://github.com/astral-sh/ruff/pull/10532))
- \[`flake8-quotes`\] Ignore `Q000`, `Q001` when string is inside forward ref ([#10585](https://github.com/astral-sh/ruff/pull/10585))
- \[`isort`\] Always place non-relative imports after relative imports ([#10669](https://github.com/astral-sh/ruff/pull/10669))
- \[`isort`\] Respect Unicode characters in import sorting ([#10529](https://github.com/astral-sh/ruff/pull/10529))
- \[`pyflakes`\] Fix F821 false negatives when `from __future__ import annotations` is active (attempt 2) ([#10524](https://github.com/astral-sh/ruff/pull/10524))
- \[`pyflakes`\] Make `unnecessary-lambda` an always-unsafe fix ([#10668](https://github.com/astral-sh/ruff/pull/10668))
- \[`pylint`\] Fixed false-positive on the rule `PLW1641` (`eq-without-hash`) ([#10566](https://github.com/astral-sh/ruff/pull/10566))
- \[`ruff`\] Fix panic in unused `# noqa` removal with multi-byte space (`RUF100`) ([#10682](https://github.com/astral-sh/ruff/pull/10682))
### Documentation
- Add PR title format to `CONTRIBUTING.md` ([#10665](https://github.com/astral-sh/ruff/pull/10665))
- Fix list markup to include blank lines required ([#10591](https://github.com/astral-sh/ruff/pull/10591))
- Put `flake8-logging` next to the other flake8 plugins in registry ([#10587](https://github.com/astral-sh/ruff/pull/10587))
- \[`flake8-bandit`\] Update warning message for rule `S305` to address insecure block cipher mode use ([#10602](https://github.com/astral-sh/ruff/pull/10602))
- \[`flake8-bugbear`\] Document use of anonymous assignment in `useless-expression` ([#10551](https://github.com/astral-sh/ruff/pull/10551))
- \[`flake8-datetimez`\] Clarify error messages and docs for `DTZ` rules ([#10621](https://github.com/astral-sh/ruff/pull/10621))
- \[`pycodestyle`\] Use same before vs. after numbers for `space-around-operator` ([#10640](https://github.com/astral-sh/ruff/pull/10640))
- \[`ruff`\] Change `quadratic-list-summation` docs to use `iadd` consistently ([#10666](https://github.com/astral-sh/ruff/pull/10666))
## 0.3.4
### Preview features
@@ -97,7 +146,7 @@
- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274))
- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265))
- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280))
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285))
## 0.3.1
@@ -205,8 +254,7 @@ This release introduces the Ruff 2024.2 style, stabilizing the following changes
Highlights include:
- Initial support formatting f-strings (in `--preview`).
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config`
argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
### Preview features
@@ -854,7 +902,7 @@ docstrings via the `docstring-code-format` setting.
- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998))
- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812))
- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
### Bug fixes
@@ -983,7 +1031,7 @@ docstrings via the `docstring-code-format` setting.
- \[`flake8-trio`\] Implement `TRIO115` ([#8486](https://github.com/astral-sh/ruff/pull/8486))
- \[`refurb`\] Implement `type-none-comparison` (`FURB169`) ([#8487](https://github.com/astral-sh/ruff/pull/8487))
- Flag all comparisons against builtin types in `E721` ([#8491](https://github.com/astral-sh/ruff/pull/8491))
- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525))
- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525))
### Formatter
@@ -1151,7 +1199,7 @@ Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blo
- Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113))
- Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140))
- Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097))
- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108))
- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108))
### Formatter
@@ -1279,7 +1327,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).*
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
### Configuration

View File

@@ -33,27 +33,18 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
## The Basics
Ruff welcomes contributions in the form of Pull Requests.
Ruff welcomes contributions in the form of pull requests.
For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.com/invite/astral-sh) to discuss your idea with the
You can also join us on [Discord](https://discord.com/invite/astral-sh) to discuss your idea with the
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
that are ready for contributions.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules from the [`flake8-pyi`](https://github.com/astral-sh/ruff/issues/848)
plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for
guidance.
If you have suggestions on how we might improve the contributing documentation, [let us know](https://github.com/astral-sh/ruff/discussions/5693)!
### Prerequisites
@@ -107,7 +98,7 @@ RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
These checks will run on GitHub Actions when you open your pull request, but running them locally
will save you time and expedite the merge process.
Note that many code changes also require updating the snapshot tests, which is done interactively
@@ -117,7 +108,14 @@ after running `cargo test` like so:
cargo insta review
```
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
If your pull request relates to a specific lint rule, include the category and rule code in the
title, as in the following examples:
- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`)
- \[`flake8-simplify`\] Detect implicit `else` cases in `needless-bool` (`SIM103`)
- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`)
Your pull request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
### Project Structure
@@ -125,8 +123,8 @@ prior to merging.
Ruff is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html),
such that all crates are contained in a flat `crates` directory.
The vast majority of the code, including all lint rules, lives in the `ruff` crate (located at
`crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you.
The vast majority of the code, including all lint rules, lives in the `ruff_linter` crate (located
at `crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you.
At the time of writing, the repository includes the following crates:
@@ -199,11 +197,14 @@ and calling out to lint rule analyzer functions as it goes.
If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. Grep
for the `Diagnostic::new` invocations to understand how other, similar rules are implemented.
Once you're satisfied with your code, add tests for your rule. See [rule testing](#rule-testing-fixtures-and-snapshots)
for more details.
Once you're satisfied with your code, add tests for your rule
(see: [rule testing](#rule-testing-fixtures-and-snapshots)), and regenerate the documentation and
associated assets (like our JSON Schema) with `cargo dev generate-all`.
Finally, regenerate the documentation and other generated assets (like our JSON Schema) with:
`cargo dev generate-all`.
Finally, submit a pull request, and include the category, rule name, and rule code in the title, as
in:
> \[`pycodestyle`\] Implement `redundant-backslash` (`E502`)
#### Rule naming convention
@@ -813,8 +814,8 @@ To understand Ruff's import categorization system, we first need to define two c
"project root".)
- "Package root": The top-most directory defining the Python package that includes a given Python
file. To find the package root for a given Python file, traverse up its parent directories until
you reach a parent directory that doesn't contain an `__init__.py` file (and isn't marked as
a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory
you reach a parent directory that doesn't contain an `__init__.py` file (and isn't in a subtree
marked as a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory
just before that, i.e., the first directory in the package.
For example, given:

278
Cargo.lock generated
View File

@@ -29,9 +29,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.80"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "argfile"
@@ -196,9 +196,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bstr"
@@ -255,9 +255,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.35"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -294,9 +294,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
@@ -358,14 +358,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -596,7 +596,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -607,7 +607,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -712,16 +712,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "env_logger"
version = "0.10.2"
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
@@ -1020,9 +1030,9 @@ dependencies = [
[[package]]
name = "indoc"
version = "2.0.4"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inotify"
@@ -1046,9 +1056,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.35.1"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2"
checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc"
dependencies = [
"console",
"globset",
@@ -1058,14 +1068,13 @@ dependencies = [
"serde",
"similar",
"walkdir",
"yaml-rust",
]
[[package]]
name = "insta-cmd"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809d3023d1d6e8d5c2206f199251f75cb26180e41f18cb0f22dd119161cb5127"
checksum = "1980f17994b79f75670aa90cfc8d35edc4aa248f16aa48b5e27835b080e452a2"
dependencies = [
"insta",
"serde",
@@ -1099,7 +1108,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -1282,7 +1291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dbd2f3cd9346422ebdc3a614aed6969d4e0b3e9c10517f33b30326acf894c11"
dependencies = [
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -1301,7 +1310,7 @@ version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"libc",
"redox_syscall",
]
@@ -1348,9 +1357,9 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.0"
version = "0.95.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -1376,9 +1385,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "mimalloc"
@@ -1461,7 +1470,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
@@ -1484,6 +1493,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "num-traits"
version = "0.2.18"
@@ -1542,6 +1560,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]]
name = "parking_lot"
version = "0.12.1"
@@ -1634,6 +1658,18 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15efd4d885c29126cc93e12af3087896e2518bd5ca0fb328c19c4ef9cecfa8be"
dependencies = [
"once_cell",
"serde",
"unicode-width",
"unscanny",
]
[[package]]
name = "pep508_rs"
version = "0.3.0"
@@ -1641,7 +1677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e"
dependencies = [
"once_cell",
"pep440_rs",
"pep440_rs 0.4.0",
"regex",
"serde",
"thiserror",
@@ -1727,7 +1763,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -1774,7 +1810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9"
dependencies = [
"indexmap",
"pep440_rs",
"pep440_rs 0.4.0",
"pep508_rs",
"serde",
"toml",
@@ -1845,9 +1881,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@@ -1885,9 +1921,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.3"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
@@ -1945,7 +1981,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -1965,18 +2001,18 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.3.4"
version = "0.3.5"
dependencies = [
"anstream",
"anyhow",
"argfile",
"bincode",
"bitflags 2.4.2",
"bitflags 2.5.0",
"cachedir",
"chrono",
"clap",
"clap_complete_command",
"clearscreen",
"colored",
"filetime",
"ignore",
"insta",
@@ -1987,6 +2023,7 @@ dependencies = [
"mimalloc",
"notify",
"num_cpus",
"owo-colors",
"path-absolutize",
"rayon",
"regex",
@@ -2127,15 +2164,15 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.3.4"
version = "0.3.5"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
"anstream",
"anyhow",
"bitflags 2.4.2",
"bitflags 2.5.0",
"chrono",
"clap",
"colored",
"fern",
"glob",
"globset",
@@ -2149,9 +2186,10 @@ dependencies = [
"memchr",
"natord",
"once_cell",
"owo-colors",
"path-absolutize",
"pathdiff",
"pep440_rs",
"pep440_rs 0.5.0",
"pyproject-toml",
"quick-junit",
"regex",
@@ -2195,7 +2233,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2222,7 +2260,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.4.2",
"bitflags 2.5.0",
"insta",
"is-macro",
"itertools 0.12.1",
@@ -2294,7 +2332,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"hexf-parse",
"itertools 0.12.1",
"lexical-parse-float",
@@ -2307,7 +2345,6 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"bstr",
"insta",
"is-macro",
@@ -2339,7 +2376,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"is-macro",
"ruff_index",
"ruff_python_ast",
@@ -2400,7 +2437,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.3.4"
version = "0.3.5"
dependencies = [
"anyhow",
"clap",
@@ -2464,7 +2501,6 @@ name = "ruff_workspace"
version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"dirs 5.0.1",
"glob",
"globset",
@@ -2472,8 +2508,9 @@ dependencies = [
"is-macro",
"itertools 0.12.1",
"log",
"owo-colors",
"path-absolutize",
"pep440_rs",
"pep440_rs 0.5.0",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2513,7 +2550,7 @@ version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
@@ -2642,7 +2679,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2658,9 +2695,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
@@ -2675,7 +2712,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2698,9 +2735,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.6.1"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a"
dependencies = [
"serde",
"serde_derive",
@@ -2709,14 +2746,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.6.1"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2745,9 +2782,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
[[package]]
name = "siphasher"
@@ -2757,9 +2794,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "smallvec"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "spin"
@@ -2809,24 +2846,24 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "strum"
version = "0.25.0"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.3"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2848,9 +2885,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.52"
version = "2.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
dependencies = [
"proc-macro2",
"quote",
@@ -2880,15 +2917,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
@@ -2930,7 +2958,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -2941,7 +2969,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
"test-case-core",
]
@@ -2962,7 +2990,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -3031,9 +3059,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.11"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
@@ -3052,9 +3080,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.7"
version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [
"indexmap",
"serde",
@@ -3083,7 +3111,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -3108,17 +3136,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
@@ -3137,7 +3154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
"nu-ansi-term 0.46.0",
"once_cell",
"regex",
"sharded-slab",
@@ -3145,18 +3162,18 @@ dependencies = [
"thread_local",
"tracing",
"tracing-core",
"tracing-log 0.2.0",
"tracing-log",
]
[[package]]
name = "tracing-tree"
version = "0.2.5"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec6adcab41b1391b08a308cc6302b79f8095d1673f6947c2dc65ffb028b0b2d"
checksum = "65139ecd2c3f6484c3b99bc01c77afe21e95473630747c7aca525e78b0666675"
dependencies = [
"nu-ansi-term",
"nu-ansi-term 0.49.0",
"tracing-core",
"tracing-log 0.1.4",
"tracing-log",
"tracing-subscriber",
]
@@ -3263,6 +3280,12 @@ dependencies = [
"rand",
]
[[package]]
name = "unscanny"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3306,9 +3329,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"getrandom",
"rand",
@@ -3318,13 +3341,13 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -3409,7 +3432,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
"wasm-bindgen-shared",
]
@@ -3443,7 +3466,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3476,7 +3499,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]
@@ -3700,15 +3723,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -3741,7 +3755,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.57",
]
[[package]]

View File

@@ -12,12 +12,13 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
aho-corasick = { version = "1.1.3" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anstream = { version = "0.6.13" }
anyhow = { version = "1.0.80" }
argfile = { version = "0.1.6" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bitflags = { version = "2.5.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
@@ -25,7 +26,6 @@ clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
colored = { version = "2.1.0" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
@@ -33,7 +33,7 @@ criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.10.1" }
env_logger = { version = "0.11.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version = "2.11.0" }
@@ -46,7 +46,7 @@ imperative = { version = "1.0.4" }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
insta-cmd = { version = "0.5.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.12.1" }
@@ -65,16 +65,17 @@ natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
num_cpus = { version = "1.16.0" }
once_cell = { version = "1.19.0" }
owo-colors = { version = "4.0.0" }
path-absolutize = { version = "3.1.1" }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pep440_rs = { version = "0.5.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.9.0" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rayon = { version = "1.8.1" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
@@ -88,11 +89,11 @@ serde_with = { version = "3.6.0", default-features = false, features = ["macros"
shellexpand = { version = "3.0.0" }
shlex = { version = "1.3.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.1" }
smallvec = { version = "1.13.2" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.51" }
strum = { version = "0.26.0", features = ["strum_macros"] }
strum_macros = { version = "0.26.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.58" }
@@ -101,7 +102,7 @@ toml = { version = "0.8.11" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-tree = { version = "0.2.4" }
tracing-tree = { version = "0.3.0" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }

View File

@@ -151,7 +151,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.3.4
rev: v0.3.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.3.4"
version = "0.3.5"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -25,6 +25,7 @@ ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" }
anstream = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
bincode = { workspace = true }
@@ -34,7 +35,6 @@ chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }
clap_complete_command = { workspace = true }
clearscreen = { workspace = true }
colored = { workspace = true }
filetime = { workspace = true }
ignore = { workspace = true }
is-macro = { workspace = true }
@@ -42,6 +42,7 @@ itertools = { workspace = true }
log = { workspace = true }
notify = { workspace = true }
num_cpus = { workspace = true }
owo-colors = { workspace = true }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { workspace = true }
regex = { workspace = true }
@@ -62,8 +63,6 @@ wild = { workspace = true }
[dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
# Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"] }
insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { workspace = true }
tempfile = { workspace = true }

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use anyhow::bail;
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser};
use colored::Colorize;
use owo_colors::OwoColorize;
use path_absolutize::path_dedot;
use regex::Regex;
use rustc_hash::FxHashMap;
@@ -1053,7 +1053,6 @@ pub enum FormatRangeParseError {
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!(
@@ -1062,7 +1061,8 @@ impl std::fmt::Display for FormatRangeParseError {
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()
end=end.to_string().green().bold(),
tip=" tip:".bold().green()
)
}
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
@@ -1148,7 +1148,8 @@ pub enum LineColumnParseError {
impl LineColumnParseError {
fn write(&self, f: &mut std::fmt::Formatter, start_range: bool) -> std::fmt::Result {
let tip = "tip:".bold().green();
let tip = "tip:".bold();
let tip = tip.green();
let range = if start_range { "start" } else { "end" };

View File

@@ -4,9 +4,9 @@ use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use ignore::Error;
use log::{debug, error, warn};
use owo_colors::OwoColorize;
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustc_hash::FxHashMap;
@@ -226,6 +226,7 @@ mod test {
use rustc_hash::FxHashMap;
use tempfile::TempDir;
use ruff_linter::colors;
use ruff_linter::message::{Emitter, EmitterContext, TextEmitter};
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
@@ -280,7 +281,7 @@ mod test {
UnsafeFixes::Enabled,
)
.unwrap();
let mut output = Vec::new();
let mut output = colors::none(Vec::new());
TextEmitter::default()
.with_show_fix_status(true)
@@ -291,7 +292,7 @@ mod test {
)
.unwrap();
let messages = String::from_utf8(output).unwrap();
let messages = String::from_utf8(output.into_inner()).unwrap();
insta::with_settings!({
omit_expression => true,

View File

@@ -2,7 +2,7 @@ use std::fs::remove_dir_all;
use std::io::{self, BufWriter, Write};
use anyhow::Result;
use colored::Colorize;
use owo_colors::OwoColorize;
use path_absolutize::path_dedot;
use walkdir::WalkDir;

View File

@@ -6,9 +6,9 @@ use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use itertools::Itertools;
use log::{error, warn};
use owo_colors::OwoColorize;
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rustc_hash::FxHashSet;
@@ -16,6 +16,7 @@ use thiserror::Error;
use tracing::debug;
use ruff_diagnostics::SourceMap;
use ruff_linter::colors;
use ruff_linter::fs;
use ruff_linter::logging::{DisplayParseError, LogLevel};
use ruff_linter::registry::Rule;
@@ -189,10 +190,10 @@ pub(crate) fn format(
match mode {
FormatMode::Write => {}
FormatMode::Check => {
results.write_changed(&mut stdout().lock())?;
results.write_changed(&mut colors::auto(stdout()).lock())?;
}
FormatMode::Diff => {
results.write_diff(&mut stdout().lock())?;
results.write_diff(&mut colors::auto(stdout()).lock())?;
}
}
@@ -200,9 +201,9 @@ pub(crate) fn format(
if config_arguments.log_level >= LogLevel::Default {
if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?;
results.write_summary(&mut colors::auto(stderr()).lock())?;
} else {
results.write_summary(&mut stdout().lock())?;
results.write_summary(&mut colors::auto(stdout()).lock())?;
}
}

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use anyhow::Result;
use log::error;
use ruff_linter::colors;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
@@ -111,7 +112,7 @@ fn format_source_code(
match &formatted {
FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => {
let mut writer = stdout().lock();
let mut writer = colors::auto(stdout()).lock();
formatted
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
@@ -120,7 +121,7 @@ fn format_source_code(
FormatMode::Diff => {
use std::io::Write;
write!(
&mut stdout().lock(),
&mut colors::auto(stdout()).lock(),
"{}",
source_kind.diff(formatted, path).unwrap()
)
@@ -130,7 +131,7 @@ fn format_source_code(
FormattedSource::Unchanged => {
// Write to stdout regardless of whether the source was formatted
if mode.is_write() {
let mut writer = stdout().lock();
let mut writer = colors::auto(stdout()).lock();
source_kind
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;

View File

@@ -8,11 +8,12 @@ use std::ops::{Add, AddAssign};
use std::path::Path;
use anyhow::{Context, Result};
use colored::Colorize;
use log::{debug, error, warn};
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_linter::colors;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
@@ -444,7 +445,7 @@ pub(crate) fn lint_stdin(
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
&mut colors::auto(io::stdout()).lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;

View File

@@ -10,10 +10,11 @@ use std::sync::mpsc::channel;
use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use owo_colors::OwoColorize;
use ruff_linter::colors;
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::SerializationFormat;
@@ -145,10 +146,6 @@ pub fn run(
}));
}
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
set_up_logging(global_options.log_level())?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
@@ -225,13 +222,12 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let mut writer: Box<dyn Write> = match cli.output_file {
Some(path) if !cli.watch => {
colored::control::set_override(false);
let file = File::create(path)?;
Box::new(BufWriter::new(file))
Box::new(BufWriter::new(colors::none(file)))
}
_ => Box::new(BufWriter::new(io::stdout())),
_ => Box::new(BufWriter::new(colors::auto(io::stdout()))),
};
let stderr_writer = Box::new(BufWriter::new(io::stderr()));
let stderr_writer = Box::new(BufWriter::new(colors::auto(io::stderr())));
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
let files = resolve_default_files(cli.files, is_stdin);

View File

@@ -1,7 +1,8 @@
use std::process::ExitCode;
use anstream::eprintln;
use clap::{Parser, Subcommand};
use colored::Colorize;
use owo_colors::OwoColorize;
use ruff::args::{Args, Command};
use ruff::{run, ExitStatus};

View File

@@ -5,8 +5,8 @@ use std::io::Write;
use anyhow::Result;
use bitflags::bitflags;
use colored::Colorize;
use itertools::{iterate, Itertools};
use owo_colors::OwoColorize;
use serde::Serialize;
use ruff_linter::fs::relativize_path;

View File

@@ -70,7 +70,7 @@ pub(crate) fn version() -> VersionInfo {
#[cfg(test)]
mod tests {
use insta::{assert_display_snapshot, assert_json_snapshot};
use insta::{assert_json_snapshot, assert_snapshot};
use super::{CommitInfo, VersionInfo};
@@ -80,7 +80,7 @@ mod tests {
version: "0.0.0".to_string(),
commit_info: None,
};
assert_display_snapshot!(version);
assert_snapshot!(version);
}
#[test]
@@ -95,7 +95,7 @@ mod tests {
commits_since_last_tag: 0,
}),
};
assert_display_snapshot!(version);
assert_snapshot!(version);
}
#[test]
@@ -110,7 +110,7 @@ mod tests {
commits_since_last_tag: 24,
}),
};
assert_display_snapshot!(version);
assert_snapshot!(version);
}
#[test]

View File

@@ -231,7 +231,7 @@ linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0

View File

@@ -25,11 +25,6 @@ pub trait Violation: Debug + PartialEq + Eq {
/// The message used to describe the violation.
fn message(&self) -> String;
/// The explanation used in documentation and elsewhere.
fn explanation() -> Option<&'static str> {
None
}
// TODO(micha): Move `fix_title` to `Fix`, add new `advice` method that is shown as an advice.
// Change the `Diagnostic` renderer to show the advice, and render the fix message after the `Suggested fix: <here>`
@@ -50,11 +45,6 @@ pub trait AlwaysFixableViolation: Debug + PartialEq + Eq {
/// The message used to describe the violation.
fn message(&self) -> String;
/// The explanation used in documentation and elsewhere.
fn explanation() -> Option<&'static str> {
None
}
/// The title displayed for the available fix.
fn fix_title(&self) -> String;
@@ -71,10 +61,6 @@ impl<V: AlwaysFixableViolation> Violation for V {
<Self as AlwaysFixableViolation>::message(self)
}
fn explanation() -> Option<&'static str> {
<Self as AlwaysFixableViolation>::explanation()
}
fn fix_title(&self) -> Option<String> {
Some(<Self as AlwaysFixableViolation>::fix_title(self))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.3.4"
version = "0.3.5"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -30,11 +30,11 @@ ruff_text_size = { path = "../ruff_text_size" }
aho-corasick = { workspace = true }
annotate-snippets = { workspace = true, features = ["color"] }
anstream = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true }
fern = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
@@ -47,6 +47,7 @@ log = { workspace = true }
memchr = { workspace = true }
natord = { workspace = true }
once_cell = { workspace = true }
owo-colors = { workspace = true }
path-absolutize = { workspace = true, features = [
"once_cell_cache",
"use_unix_paths_on_wasm",
@@ -75,8 +76,6 @@ url = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
test-case = { workspace = true }
# Disable colored output in tests
colored = { workspace = true, features = ["no-color"] }
[features]
default = []

View File

@@ -31,8 +31,7 @@ def function(
kwonly_nonboolvalued_boolhint: bool = 1,
kwonly_nonboolvalued_boolstrhint: "bool" = 1,
**kw,
):
...
): ...
def used(do):
@@ -131,4 +130,27 @@ class Fit:
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)
# https://github.com/astral-sh/ruff/issues/10356
from django.db.models import Case, Q, Value, When
qs.annotate(
is_foo_or_bar=Case(
When(Q(is_foo=True) | Q(is_bar=True)),
then=Value(True),
),
default=Value(False),
)
# https://github.com/astral-sh/ruff/issues/10485
from pydantic import Field
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
foo: bool = Field(True, exclude=True)

View File

@@ -174,6 +174,49 @@ for (_key1, _key2), (_value1, _value2) in groupby(
collect_shop_items("Jane", group[1])
collect_shop_items("Joe", group[1])
# Shouldn't trigger the warning when there is a continue, break statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
continue
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
break
collect_shop_items(shopper, section_items)
# Shouldn't trigger the warning when there is a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
collect_shop_items(shopper, section_items)
# Should trigger the warning for duplicate access, even if is a return statement after.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
# Should trigger the warning for duplicate access, even if is a return in another branch.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
# Should trigger, since only one branch has a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
# Let's redefine the `groupby` function to make sure we pick up the correct one.
# NOTE: This should always be at the end of the file.

View File

@@ -1,20 +1,30 @@
x = set(x for x in range(3))
x = set(x for x in range(3))
y = f"{set(a if a < 6 else 0 for a in range(3))}"
_ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
print(f"Hello {set(a for a in range(3))} World")
# Cannot conbime with C416. Should use set comprehension here.
even_nums = set(2 * x for x in range(3))
odd_nums = set(
2 * x + 1 for x in range(3)
)
small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
def f(x):
return x
print(f'Hello {set(a for a in "abc")} World')
print(f"Hello {set(a for a in 'abc')} World")
print(f"Hello {set(f(a) for a in 'abc')} World")
print(f"Hello { set(f(a) for a in 'abc') } World")
# Short-circuit case, combine with C416 and should produce x = set(range(3))
x = set(x for x in range(3))
x = set(
x for x in range(3)
)
print(f"Hello {set(a for a in range(3))} World")
print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
# The fix generated for this diagnostic is incorrect, as we add additional space
# around the set comprehension.
print(f"{ {set(a for a in 'abc')} }")
# Not built-in set.
def set(*args, **kwargs):
return None
set(2 * x for x in range(3))
set(x for x in range(3))

View File

@@ -13,6 +13,10 @@ all(x.id for x in bar)
all(x.id for x in bar)
any(x.id for x in bar)
all((x.id for x in bar))
# we don't lint on these in stable yet
sum([x.val for x in bar])
min([x.val for x in bar])
max([x.val for x in bar])
async def f() -> bool:

View File

@@ -0,0 +1,8 @@
sum([x.val for x in bar])
min([x.val for x in bar])
max([x.val for x in bar])
# Ok
sum(x.val for x in bar)
min(x.val for x in bar)
max(x.val for x in bar)

View File

@@ -0,0 +1,3 @@
# no lint if shadowed
def all(x): pass
all([x.id for x in bar])

View File

@@ -33,3 +33,9 @@ from datetime import datetime
# no replace orastimezone unqualified
datetime.strptime("something", "something")
# F-strings
datetime.strptime("something", f"%Y-%m-%dT%H:%M:%S{('.%f' if millis else '')}%z")
datetime.strptime("something", f"%Y-%m-%d %H:%M:%S%z")
# F-string is implicitly concatenated to another string
datetime.strptime("something", f"%Y-%m-%dT%H:%M:%S{('.%f' if millis else '')}" "%z")

View File

@@ -21,6 +21,7 @@ def unconventional_aliases():
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt

View File

@@ -0,0 +1,10 @@
def no_alias():
from django.conf import settings
def conventional_alias():
from django.conf import settings as settings
def unconventional_alias():
from django.conf import settings as s

View File

@@ -0,0 +1,14 @@
"""Tests to ensure we correctly rename references inside `__all__`"""
from collections.abc import Set
__all__ = ["Set"]
if True:
__all__ += [r'''Set''']
if 1:
__all__ += ["S" "e" "t"]
if not False:
__all__ += ["Se" 't']

View File

@@ -0,0 +1,14 @@
"""Tests to ensure we correctly rename references inside `__all__`"""
from collections.abc import Set
__all__ = ["Set"]
if True:
__all__ += [r'''Set''']
if 1:
__all__ += ["S" "e" "t"]
if not False:
__all__ += ["Se" 't']

View File

@@ -0,0 +1,6 @@
"""
Tests for PYI025 where the import is marked as re-exported
through usage of a "redundant" `import Set as Set` alias
"""
from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe

View File

@@ -0,0 +1,6 @@
"""
Tests for PYI025 where the import is marked as re-exported
through usage of a "redundant" `import Set as Set` alias
"""
from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe

View File

@@ -51,3 +51,8 @@ def test_error_parentheses_trailing_comma(x):
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...
@pytest.mark.parametrize('data, spec', [(1.0, 1.0), (1.0, 1.0)])
def test_numbers(data, spec):
...

View File

@@ -0,0 +1,7 @@
"""This is a docstring."""
this_is_an_inline_string = "double quote string"
this_is_a_multiline_string = """
double quote string
"""

View File

@@ -2,3 +2,6 @@ this_should_be_linted = 'single quote string'
this_should_be_linted = u'double quote string'
this_should_be_linted = f'double quote string'
this_should_be_linted = f'double {"quote"} string'
# https://github.com/astral-sh/ruff/issues/10546
x: "Literal['foo', 'bar']"

View File

@@ -406,3 +406,18 @@ def foo():
with contextlib.suppress(Exception):
y = 2
return y
# See: https://github.com/astral-sh/ruff/issues/10732
def func(a: dict[str, int]) -> list[dict[str, int]]:
services: list[dict[str, int]]
if "services" in a:
services = a["services"]
return services
# See: https://github.com/astral-sh/ruff/issues/10732
def func(a: dict[str, int]) -> list[dict[str, int]]:
if "services" in a:
services = a["services"]
return services

View File

@@ -3,3 +3,5 @@ import ruff
import leading_prefix
import os
from . import leading_prefix
from .. import trailing_prefix
from ruff import check

View File

@@ -0,0 +1,6 @@
from astropy.constants import hbar as
from numpy import pi as π
import numpy as ℂℇℊℋℌℍℎℐℑℒℓℕℤΩℨKÅℬℭℯℰℱℹℴ
import numpy as CƐgHHHhIILlNZΩZKÅBCeEFio
h = 2 * π *

View File

@@ -104,3 +104,5 @@ def func():
np.unicode_("asf")
np.who()
np.row_stack(([1,2], [3,4]))

View File

@@ -21,3 +21,10 @@ class D(TypedDict):
mixedCase: bool
_mixedCase: list
mixed_Case: set
class E(D):
lower: int
CONSTANT: str
mixedCase: bool
_mixedCase: list
mixed_Case: set

View File

@@ -0,0 +1,228 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "palRUQyD-U6u"
},
"outputs": [],
"source": [
"some_string = \"123123\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "UWdDLRyf-Zz0"
},
"outputs": [],
"source": [
"some_computation = 1 + 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "YreT1sTr-c32"
},
"outputs": [],
"source": [
"some_computation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "V48ppml7-h0f"
},
"outputs": [],
"source": [
"def fn():\n",
" print(\"Hey!\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "cscw_8Xv-lYQ"
},
"outputs": [],
"source": [
"fn()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E301\n",
"class Class:\n",
" \"\"\"Class for minimal repo.\"\"\"\n",
"\n",
" def method(cls) -> None:\n",
" pass\n",
" @classmethod\n",
" def cls_method(cls) -> None:\n",
" pass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E302\n",
"def a():\n",
" pass\n",
"\n",
"def b():\n",
" pass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E303\n",
"def fn():\n",
" _ = None\n",
"\n",
"\n",
" # arbitrary comment\n",
"\n",
" def inner(): # E306 not expected (pycodestyle detects E306)\n",
" pass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E303"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n",
"\n",
"\n",
"\n",
"def fn():\n",
"\tpass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E304\n",
"@decorator\n",
"\n",
"def function():\n",
" pass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E305:7:1\n",
"def fn():\n",
" print()\n",
"\n",
" # comment\n",
"\n",
" # another comment\n",
"fn()\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# E306:3:5\n",
"def a():\n",
" x = 1\n",
" def b():\n",
" pass\n",
"# end"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ok\n",
"def function1():\n",
"\tpass\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n",
"def function2():\n",
"\tpass\n",
"# end"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"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.11.7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -87,6 +87,37 @@
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "a51463ee-091c-44b4-9069-c03bf7e3bf83",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"import pathlib"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ddc937e-6c19-475f-b108-9405aa1af4f1",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "285041d2-a76c-4ff3-8ff2-0131bbf66016",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"%%time\n",
"import pathlib"
]
}
],
"metadata": {

View File

@@ -60,7 +60,7 @@ class Scope:
class Scope:
from typing import Callable
# E731
# OK
f: Callable[[int], int] = lambda x: 2 * x
@@ -147,3 +147,12 @@ def scope():
f = lambda: (
i := 1,
)
from dataclasses import dataclass
from typing import Callable
@dataclass
class FilterDataclass:
# OK
filter: Callable[[str], bool] = lambda _: True

View File

@@ -33,3 +33,16 @@ class MyClass:
baz: MyClass
eggs = baz # Still invalid even when `__future__.annotations` are enabled
eggs = "baz" # always okay
# Forward references:
MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled
MaybeDStr2: TypeAlias = Optional["DStr"] # always okay
DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled
DStr2: TypeAlias = Union["D", str] # always okay
class D: ...
# More circular references
class Leaf: ...
class Tree(list[Tree | Leaf]): ... # Still invalid even when `__future__.annotations` are enabled
class Tree2(list["Tree | Leaf"]): ... # always okay

View File

@@ -5,6 +5,7 @@ class Person: # [eq-without-hash]
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
@@ -16,8 +17,24 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None
class SingleClass:
def __eq__(self, other):
return True
def __hash__(self):
return 7
class ChildClass(SingleClass):
def __eq__(self, other):
return True
__hash__ = SingleClass.__hash__

View File

@@ -0,0 +1,60 @@
# Errors
nums = {1, 2, 3}
for num in nums:
nums.add(num + 1)
animals = {"dog", "cat", "cow"}
for animal in animals:
animals.pop("cow")
fruits = {"apple", "orange", "grape"}
for fruit in fruits:
fruits.clear()
planets = {"mercury", "venus", "earth"}
for planet in planets:
planets.discard("mercury")
colors = {"red", "green", "blue"}
for color in colors:
colors.remove("red")
odds = {1, 3, 5}
for num in odds:
if num > 1:
odds.add(num + 1)
# OK
nums = {1, 2, 3}
for num in nums.copy():
nums.add(nums + 3)
animals = {"dog", "cat", "cow"}
for animal in animals:
print(animals - {animal})
fruits = {"apple", "orange", "grape"}
temp_fruits = set()
for fruit in fruits:
temp_fruits.add(fruit)
temp_fruits.remove(fruit)
temp_fruits.clear(fruit)
colors = {"red", "green", "blue"}
def add_colors():
colors = {"cyan", "magenta", "yellow"}
for color in colors:
def add_color():
global colors
colors.add(color)
add_color()
add_colors()
print(colors)

View File

@@ -20,7 +20,7 @@ class Board:
def place(self, position):
pass
@singledispatch
@singledispatch # [singledispatch-method]
@staticmethod
def do(position):
pass

View File

@@ -17,7 +17,7 @@ class Board:
def move(self, position):
pass
@singledispatchmethod # [singledispatchmethod-function]
@singledispatchmethod # Ok
@staticmethod
def do(position):
pass

View File

@@ -0,0 +1,61 @@
# Errors
s = set()
for x in [1, 2, 3]:
s.add(x)
for x in {1, 2, 3}:
s.add(x)
for x in (1, 2, 3):
s.add(x)
for x in (1, 2, 3):
s.discard(x)
for x in (1, 2, 3):
s.add(x + 1)
for x, y in ((1, 2), (3, 4)):
s.add((x, y))
num = 123
for x in (1, 2, 3):
s.add(num)
for x in (1, 2, 3):
s.add((num, x))
for x in (1, 2, 3):
s.add(x + num)
# False negative
class C:
s: set[int]
c = C()
for x in (1, 2, 3):
c.s.add(x)
# Ok
s.update(x for x in (1, 2, 3))
for x in (1, 2, 3):
s.add(x)
else:
pass
async def f(y):
async for x in y:
s.add(x)
def g():
for x in (set(),):
x.add(x)

View File

@@ -0,0 +1,17 @@
import decimal
from decimal import Decimal
# Errors
Decimal("0")
Decimal("-42")
Decimal(float("Infinity"))
Decimal(float("-Infinity"))
Decimal(float("inf"))
Decimal(float("-inf"))
Decimal(float("nan"))
decimal.Decimal("0")
# OK
Decimal(0)
Decimal("Infinity")
decimal.Decimal(0)

View File

@@ -0,0 +1,34 @@
from decimal import Decimal
from fractions import Fraction
import decimal
import fractions
# Errors
_ = Fraction.from_float(0.1)
_ = Fraction.from_float(-0.5)
_ = Fraction.from_float(5.0)
_ = fractions.Fraction.from_float(4.2)
_ = Fraction.from_decimal(Decimal("4.2"))
_ = Fraction.from_decimal(Decimal("-4.2"))
_ = Fraction.from_decimal(Decimal.from_float(4.2))
_ = Decimal.from_float(0.1)
_ = Decimal.from_float(-0.5)
_ = Decimal.from_float(5.0)
_ = decimal.Decimal.from_float(4.2)
_ = Decimal.from_float(float("inf"))
_ = Decimal.from_float(float("-inf"))
_ = Decimal.from_float(float("Infinity"))
_ = Decimal.from_float(float("-Infinity"))
_ = Decimal.from_float(float("nan"))
# OK
_ = Fraction(0.1)
_ = Fraction(-0.5)
_ = Fraction(5.0)
_ = fractions.Fraction(4.2)
_ = Fraction(Decimal("4.2"))
_ = Fraction(Decimal("-4.2"))
_ = Decimal(0.1)
_ = Decimal(-0.5)
_ = Decimal(5.0)
_ = decimal.Decimal(4.2)

View File

@@ -0,0 +1,29 @@
# Errors
_ = int("0b1010"[2:], 2)
_ = int("0o777"[2:], 8)
_ = int("0xFFFF"[2:], 16)
b = "0b11"
_ = int(b[2:], 2)
_ = int("0xFFFF"[2:], base=16)
_ = int(b"0xFFFF"[2:], 16)
def get_str():
return "0xFFF"
_ = int(get_str()[2:], 16)
# OK
_ = int("0b1100", 0)
_ = int("123", 3)
_ = int("123", 10)
_ = int("0b1010"[3:], 2)
_ = int("0b1010"[:2], 2)
_ = int("12345"[2:])
_ = int("12345"[2:], xyz=1) # type: ignore

View File

@@ -100,3 +100,9 @@ def f():
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa
def f():
# Invalid - nonexistant error code with multibyte character
d = 1 #…noqa: F841, E50
e = 1 #…noqa: E50

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::str::raw_contents_range;
use ruff_python_ast::{all::DunderAllName, str::raw_contents_range};
use ruff_text_size::{Ranged, TextRange};
use ruff_python_semantic::{
@@ -93,7 +93,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
}
// Compute visibility of all definitions.
let exports: Option<Vec<&str>> = {
let exports: Option<Vec<DunderAllName>> = {
checker
.semantic
.global_scope()

View File

@@ -223,14 +223,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.current_scope().kind
if let ScopeKind::Class(class_def) = &checker.semantic.current_scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker,
expr,
id,
arguments.as_deref(),
checker, expr, id, class_def,
);
}
}
@@ -709,8 +705,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
args,
);
}
if checker.enabled(Rule::UnnecessaryComprehensionAnyAll) {
flake8_comprehensions::rules::unnecessary_comprehension_any_all(
if checker.enabled(Rule::UnnecessaryComprehensionInCall) {
flake8_comprehensions::rules::unnecessary_comprehension_in_call(
checker, expr, func, args, keywords,
);
}
@@ -926,6 +922,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::RedundantLogBase) {
refurb::rules::redundant_log_base(checker, call);
}
if checker.enabled(Rule::VerboseDecimalConstructor) {
refurb::rules::verbose_decimal_constructor(checker, call);
}
if checker.enabled(Rule::UnnecessaryFromFloat) {
refurb::rules::unnecessary_from_float(checker, call);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
@@ -971,6 +973,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
}
if checker.enabled(Rule::IntOnSlicedStr) {
refurb::rules::int_on_sliced_str(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[

View File

@@ -1293,6 +1293,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ManualDictComprehension) {
perflint::rules::manual_dict_comprehension(checker, target, body);
}
if checker.enabled(Rule::ModifiedIteratingSet) {
pylint::rules::modified_iterating_set(checker, for_stmt);
}
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter, body);
}
@@ -1315,6 +1318,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
if checker.enabled(Rule::ForLoopSetMutations) {
refurb::rules::for_loop_set_mutations(checker, for_stmt);
}
}
}
Stmt::Try(ast::StmtTry {

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi, ruff};
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
@@ -23,4 +23,14 @@ pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
}
}
if checker.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
]) {
flake8_quotes::rules::check_string_quotes(checker, string_like);
}
if checker.enabled(Rule::UnnecessaryEscapedQuote) {
flake8_quotes::rules::unnecessary_escaped_quote(checker, string_like);
}
}

View File

@@ -31,8 +31,9 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use ruff_python_ast::{
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, Keyword,
MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
self as ast, all::DunderAllName, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt,
Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -364,6 +365,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !(self.semantic.seen_import_boundary()
|| stmt.is_ipy_escape_command_stmt()
|| helpers::is_assignment_to_a_dunder(stmt)
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
@@ -937,6 +939,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
&& (self.semantic.in_annotation() || self.source_type.is_stub())
{
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
self.visit.string_type_definitions.push((
@@ -2097,33 +2100,32 @@ impl<'a> Checker<'a> {
fn visit_exports(&mut self) {
let snapshot = self.semantic.snapshot();
let exports: Vec<(&str, TextRange)> = self
let exports: Vec<DunderAllName> = self
.semantic
.global_scope()
.get_all("__all__")
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range())))
}
BindingKind::Export(Export { names }) => Some(names.iter().copied()),
_ => None,
})
.flatten()
.collect();
for (name, range) in exports {
for export in exports {
let (name, range) = (export.name(), export.range());
if let Some(binding_id) = self.semantic.global_scope().get(name) {
self.semantic.flags |= SemanticModelFlags::DUNDER_ALL_DEFINITION;
// Mark anything referenced in `__all__` as used.
// TODO(charlie): `range` here should be the range of the name in `__all__`, not
// the range of `__all__` itself.
self.semantic
.add_global_reference(binding_id, ExprContext::Load, range);
self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION;
} else {
if self.semantic.global_scope().uses_star_imports() {
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: (*name).to_string(),
name: name.to_string(),
},
range,
));
@@ -2133,7 +2135,7 @@ impl<'a> Checker<'a> {
if !self.path.ends_with("__init__.py") {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedExport {
name: (*name).to_string(),
name: name.to_string(),
},
range,
));

View File

@@ -41,7 +41,7 @@ pub(crate) fn check_tokens(
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(locator, stylist, settings, source_type)
BlankLinesChecker::new(locator, stylist, settings, source_type, cell_offsets)
.check_lines(tokens, &mut diagnostics);
}
@@ -126,18 +126,6 @@ pub(crate) fn check_tokens(
flake8_quotes::rules::avoidable_escaped_quote(&mut diagnostics, tokens, locator, settings);
}
if settings.rules.enabled(Rule::UnnecessaryEscapedQuote) {
flake8_quotes::rules::unnecessary_escaped_quote(&mut diagnostics, tokens, locator);
}
if settings.rules.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
]) {
flake8_quotes::rules::check_string_quotes(&mut diagnostics, tokens, locator, settings);
}
if settings.rules.any_enabled(&[
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,

View File

@@ -265,6 +265,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E2513") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterEsc),
(Pylint, "E2514") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterNul),
(Pylint, "E2515") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterZeroWidthSpace),
(Pylint, "E4703") => (RuleGroup::Preview, rules::pylint::rules::ModifiedIteratingSet),
(Pylint, "R0124") => (RuleGroup::Stable, rules::pylint::rules::ComparisonWithItself),
(Pylint, "R0133") => (RuleGroup::Stable, rules::pylint::rules::ComparisonOfConstant),
(Pylint, "R0202") => (RuleGroup::Preview, rules::pylint::rules::NoClassmethodDecorator),
@@ -397,7 +398,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Comprehensions, "16") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehension),
(Flake8Comprehensions, "17") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryMap),
(Flake8Comprehensions, "18") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall),
(Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionAnyAll),
(Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall),
// flake8-debugger
(Flake8Debugger, "0") => (RuleGroup::Stable, rules::flake8_debugger::rules::Debugger),
@@ -1044,11 +1045,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Preview, rules::refurb::rules::IfExprMinMax),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),
(Refurb, "167") => (RuleGroup::Preview, rules::refurb::rules::RegexFlagAlias),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),

View File

@@ -0,0 +1,19 @@
use anstream::stream::RawStream;
use anstream::{AutoStream, ColorChoice};
pub fn none<S: RawStream>(stream: S) -> AutoStream<S> {
AutoStream::new(stream, ColorChoice::Never)
}
pub fn auto<S: RawStream>(stream: S) -> AutoStream<S> {
let choice = choice(&stream);
AutoStream::new(stream, choice)
}
pub fn choice<S: RawStream>(stream: &S) -> ColorChoice {
AutoStream::choice(stream)
}
pub fn enabled<S: RawStream>(stream: &S) -> bool {
choice(stream) != ColorChoice::Never
}

View File

@@ -83,6 +83,7 @@ pub(crate) fn delete_comment(range: TextRange, locator: &Locator) -> Edit {
}
// Ex) `x = 1 # noqa`
else if range.end() + trailing_space_len == line_range.end() {
// Replace `x = 1 # noqa` with `x = 1`.
Edit::deletion(range.start() - leading_space_len, line_range.end())
}
// Ex) `x = 1 # noqa # type: ignore`
@@ -93,13 +94,15 @@ pub(crate) fn delete_comment(range: TextRange, locator: &Locator) -> Edit {
))
.starts_with('#')
{
// Replace `# noqa # type: ignore` with `# type: ignore`.
Edit::deletion(range.start(), range.end() + trailing_space_len)
}
// Ex) `x = 1 # noqa here`
else {
Edit::deletion(
range.start() + "# ".text_len(),
range.end() + trailing_space_len,
// Replace `# noqa here` with `# here`.
Edit::range_replacement(
"# ".to_string(),
TextRange::new(range.start(), range.end() + trailing_space_len),
)
}
}

View File

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

View File

@@ -2,10 +2,11 @@ use std::borrow::Cow;
use std::ops::Deref;
use std::path::Path;
use anstream::eprintln;
use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use log::error;
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;

View File

@@ -3,15 +3,16 @@ use std::path::{Path, PathBuf};
use std::sync::Mutex;
use anyhow::Result;
use colored::Colorize;
use fern;
use log::Level;
use once_cell::sync::Lazy;
use owo_colors::OwoColorize;
use ruff_python_parser::{ParseError, ParseErrorType};
use rustc_hash::FxHashSet;
use ruff_source_file::{LineIndex, OneIndexed, SourceCode, SourceLocation};
use crate::colors;
use crate::fs;
use crate::source_kind::SourceKind;
use ruff_notebook::Notebook;
@@ -22,7 +23,7 @@ pub static IDENTIFIERS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::defaul
#[macro_export]
macro_rules! warn_user_once_by_id {
($id:expr, $($arg:tt)*) => {
use colored::Colorize;
use owo_colors::OwoColorize;
use log::warn;
if let Ok(mut states) = $crate::logging::IDENTIFIERS.lock() {
@@ -42,7 +43,7 @@ pub static MESSAGES: Lazy<Mutex<FxHashSet<String>>> = Lazy::new(Mutex::default);
#[macro_export]
macro_rules! warn_user_once_by_message {
($($arg:tt)*) => {
use colored::Colorize;
use owo_colors::OwoColorize;
use log::warn;
if let Ok(mut states) = $crate::logging::MESSAGES.lock() {
@@ -59,7 +60,7 @@ macro_rules! warn_user_once_by_message {
#[macro_export]
macro_rules! warn_user_once {
($($arg:tt)*) => {
use colored::Colorize;
use owo_colors::OwoColorize;
use log::warn;
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
@@ -73,7 +74,7 @@ macro_rules! warn_user_once {
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {{
use colored::Colorize;
use owo_colors::OwoColorize;
use log::warn;
let message = format!("{}", format_args!($($arg)*));
@@ -152,7 +153,10 @@ pub fn set_up_logging(level: LogLevel) -> Result<()> {
})
.level(level.level_filter())
.level_for("globset", log::LevelFilter::Warn)
.chain(std::io::stderr())
.chain(fern::Output::writer(
Box::new(colors::auto(std::io::stderr())),
"\n",
))
.apply()?;
Ok(())
}

View File

@@ -1,7 +1,7 @@
use std::fmt::{Display, Formatter};
use std::num::NonZeroUsize;
use colored::{Color, ColoredString, Colorize, Styles};
use owo_colors::{OwoColorize, Style};
use ruff_text_size::{Ranged, TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
@@ -81,7 +81,7 @@ impl Display for Diff<'_> {
ChangeTag::Equal => " ",
};
let line_style = LineStyle::from(change.tag());
let line_style = diff_line_style(change.tag());
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
@@ -97,14 +97,14 @@ impl Display for Diff<'_> {
index: new_index,
width: digit_with
},
line_style.apply_to(sign).bold()
sign.style(line_style).bold()
)?;
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
write!(f, "{}", line_style.apply_to(&value).underline().on_black())?;
write!(f, "{}", value.style(line_style).underline().on_black())?;
} else {
write!(f, "{}", line_style.apply_to(&value))?;
write!(f, "{}", value.style(line_style))?;
}
}
if change.missing_newline() {
@@ -118,52 +118,11 @@ impl Display for Diff<'_> {
}
}
struct LineStyle {
fgcolor: Option<Color>,
style: Option<Styles>,
}
impl LineStyle {
fn apply_to(&self, input: &str) -> ColoredString {
let mut colored = ColoredString::from(input);
if let Some(color) = self.fgcolor {
colored = colored.color(color);
}
if let Some(style) = self.style {
match style {
Styles::Clear => colored.clear(),
Styles::Bold => colored.bold(),
Styles::Dimmed => colored.dimmed(),
Styles::Underline => colored.underline(),
Styles::Reversed => colored.reversed(),
Styles::Italic => colored.italic(),
Styles::Blink => colored.blink(),
Styles::Hidden => colored.hidden(),
Styles::Strikethrough => colored.strikethrough(),
}
} else {
colored
}
}
}
impl From<ChangeTag> for LineStyle {
fn from(value: ChangeTag) -> Self {
match value {
ChangeTag::Equal => LineStyle {
fgcolor: None,
style: Some(Styles::Dimmed),
},
ChangeTag::Delete => LineStyle {
fgcolor: Some(Color::Red),
style: None,
},
ChangeTag::Insert => LineStyle {
fgcolor: Some(Color::Green),
style: None,
},
}
fn diff_line_style(change_tag: ChangeTag) -> Style {
match change_tag {
ChangeTag::Equal => Style::new().dimmed(),
ChangeTag::Delete => Style::new().red(),
ChangeTag::Insert => Style::new().green(),
}
}

View File

@@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter};
use std::io::Write;
use std::num::NonZeroUsize;
use colored::Colorize;
use owo_colors::OwoColorize;
use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;

View File

@@ -156,6 +156,7 @@ mod tests {
use ruff_source_file::{OneIndexed, SourceFileBuilder};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::colors;
use crate::message::{Emitter, EmitterContext, Message};
pub(super) fn create_messages() -> Vec<Message> {
@@ -337,10 +338,10 @@ def foo():
) -> String {
let notebook_indexes = FxHashMap::default();
let context = EmitterContext::new(&notebook_indexes);
let mut output: Vec<u8> = Vec::new();
let mut output = colors::none(Vec::new());
emitter.emit(&mut output, messages, &context).unwrap();
String::from_utf8(output).expect("Output to be valid UTF-8")
String::from_utf8(output.into_inner()).expect("Output to be valid UTF-8")
}
pub(super) fn capture_emitter_notebook_output(
@@ -349,9 +350,9 @@ def foo():
notebook_indexes: &FxHashMap<String, NotebookIndex>,
) -> String {
let context = EmitterContext::new(notebook_indexes);
let mut output: Vec<u8> = Vec::new();
let mut output = colors::none(Vec::new());
emitter.emit(&mut output, messages, &context).unwrap();
String::from_utf8(output).expect("Output to be valid UTF-8")
String::from_utf8(output.into_inner()).expect("Output to be valid UTF-8")
}
}

View File

@@ -5,12 +5,13 @@ use std::io::Write;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use owo_colors::OwoColorize;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::colors;
use crate::fs::relativize_path;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
@@ -284,10 +285,7 @@ impl Display for MessageCodeFrame<'_> {
}],
footer,
opt: FormatOptions {
#[cfg(test)]
color: false,
#[cfg(not(test))]
color: colored::control::SHOULD_COLORIZE.should_colorize(),
color: colors::enabled(&std::io::stdout()),
..FormatOptions::default()
},
};

View File

@@ -26,17 +26,14 @@ use std::path::{Path, PathBuf};
/// Return `true` if the directory at the given `Path` appears to be a Python
/// package.
pub fn is_package(path: &Path, namespace_packages: &[PathBuf]) -> bool {
path.join("__init__.py").is_file()
|| namespace_packages
.iter()
.any(|namespace_package| namespace_package == path)
namespace_packages
.iter()
.any(|namespace_package| path.starts_with(namespace_package))
|| path.join("__init__.py").is_file()
}
/// Return the package root for the given Python file.
pub fn detect_package_root<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
) -> Option<&'a Path> {
/// Return the package root for the given path to a directory with Python file.
pub fn detect_package_root<'a>(path: &'a Path, namespace_packages: &[PathBuf]) -> Option<&'a Path> {
let mut current = None;
for parent in path.ancestors() {
if !is_package(parent, namespace_packages) {
@@ -84,4 +81,39 @@ mod tests {
None,
);
}
#[test]
fn package_detection_with_namespace_packages() {
assert_eq!(
detect_package_root(&test_resource_path("project/python_modules/core/core"), &[],),
Some(test_resource_path("project/python_modules/core/core").as_path())
);
assert_eq!(
detect_package_root(
&test_resource_path("project/python_modules/core/core"),
&[test_resource_path("project/python_modules/core"),],
),
Some(test_resource_path("project/python_modules/core").as_path())
);
assert_eq!(
detect_package_root(
&test_resource_path("project/python_modules/core/core"),
&[
test_resource_path("project/python_modules/core"),
test_resource_path("project/python_modules"),
],
),
Some(test_resource_path("project/python_modules").as_path())
);
assert_eq!(
detect_package_root(
&test_resource_path("project/python_modules/core/core"),
&[test_resource_path("project/python_modules"),],
),
Some(test_resource_path("project/python_modules").as_path())
);
}
}

View File

@@ -1,5 +1,5 @@
use colored::Colorize;
use log::warn;
use owo_colors::OwoColorize;
use pyproject_toml::PyProjectToml;
use ruff_text_size::{TextRange, TextSize};

View File

@@ -115,6 +115,9 @@ pub enum Linter {
/// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
#[prefix = "ICN"]
Flake8ImportConventions,
/// [flake8-logging](https://pypi.org/project/flake8-logging/)
#[prefix = "LOG"]
Flake8Logging,
/// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
#[prefix = "G"]
Flake8LoggingFormat,
@@ -202,9 +205,6 @@ pub enum Linter {
/// [refurb](https://pypi.org/project/refurb/)
#[prefix = "FURB"]
Refurb,
/// [flake8-logging](https://pypi.org/project/flake8-logging/)
#[prefix = "LOG"]
Flake8Logging,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
@@ -257,9 +257,6 @@ impl Rule {
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AvoidableEscapedQuote
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::BlankLineAfterDecorator

View File

@@ -4,8 +4,9 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextSize};
pub(crate) struct Renamer;
@@ -104,6 +105,7 @@ impl Renamer {
target: &str,
scope: &Scope,
semantic: &SemanticModel,
stylist: &Stylist,
) -> Result<(Edit, Vec<Edit>)> {
let mut edits = vec![];
@@ -130,7 +132,9 @@ impl Renamer {
});
let scope = scope_id.map_or(scope, |scope_id| &semantic.scopes[scope_id]);
edits.extend(Renamer::rename_in_scope(name, target, scope, semantic));
edits.extend(Renamer::rename_in_scope(
name, target, scope, semantic, stylist,
));
// Find any scopes in which the symbol is referenced as `nonlocal` or `global`. For example,
// given:
@@ -160,7 +164,9 @@ impl Renamer {
.copied()
{
let scope = &semantic.scopes[scope_id];
edits.extend(Renamer::rename_in_scope(name, target, scope, semantic));
edits.extend(Renamer::rename_in_scope(
name, target, scope, semantic, stylist,
));
}
// Deduplicate any edits.
@@ -180,6 +186,7 @@ impl Renamer {
target: &str,
scope: &Scope,
semantic: &SemanticModel,
stylist: &Stylist,
) -> Vec<Edit> {
let mut edits = vec![];
@@ -202,7 +209,17 @@ impl Renamer {
// Rename the references to the binding.
edits.extend(binding.references().map(|reference_id| {
let reference = semantic.reference(reference_id);
Edit::range_replacement(target.to_string(), reference.range())
let replacement = {
if reference.in_dunder_all_definition() {
debug_assert!(!reference.range().is_empty());
let quote = stylist.quote();
format!("{quote}{target}{quote}")
} else {
debug_assert_eq!(TextSize::of(name), reference.range().len());
target.to_string()
}
};
Edit::range_replacement(replacement, reference.range())
}));
}
}

View File

@@ -220,7 +220,7 @@ pub struct SuspiciousInsecureCipherModeUsage;
impl Violation for SuspiciousInsecureCipherModeUsage {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure cipher mode, replace with a known secure cipher such as AES")
format!("Use of insecure block cipher mode, replace with a known secure mode such as CBC or CTR")
}
}

View File

@@ -1,4 +1,9 @@
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::settings::LinterSettings;
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
@@ -43,6 +48,24 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
)
}
/// Returns `true` if a call is allowed by the user to use a boolean trap.
pub(super) fn is_user_allowed_func_call(
call: &ast::ExprCall,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> bool {
semantic
.resolve_qualified_name(call.func.as_ref())
.is_some_and(|qualified_name| {
settings
.flake8_boolean_trap
.extend_allowed_calls
.iter()
.map(|target| QualifiedName::from_dotted_name(target))
.any(|target| qualified_name == target)
})
}
/// Returns `true` if a function definition is allowed to use a boolean trap.
pub(super) fn is_allowed_func_def(name: &str) -> bool {
matches!(name, "__setitem__" | "__post_init__")
@@ -51,7 +74,7 @@ pub(super) fn is_allowed_func_def(name: &str) -> bool {
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
pub(super) fn allow_boolean_trap(call: &ast::ExprCall, checker: &Checker) -> bool {
let func_name = match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr.as_str(),
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
@@ -76,5 +99,10 @@ pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
}
}
// If the call is explicitly allowed by the user, then the boolean trap is allowed.
if is_user_allowed_func_call(call, checker.semantic(), checker.settings) {
return true;
}
false
}

View File

@@ -1,6 +1,7 @@
//! Rules from [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/).
mod helpers;
pub(crate) mod rules;
pub mod settings;
#[cfg(test)]
mod tests {
@@ -11,6 +12,7 @@ mod tests {
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -44,4 +46,22 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_allowed_callable() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_boolean_trap/FBT.py"),
&LinterSettings {
flake8_boolean_trap: super::settings::Settings {
extend_allowed_calls: vec![
"django.db.models.Value".to_string(),
"pydantic.Field".to_string(),
],
},
..LinterSettings::for_rule(Rule::BooleanPositionalValueInCall)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -9,6 +9,9 @@ use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap;
/// ## What it does
/// Checks for boolean positional arguments in function calls.
///
/// Some functions are whitelisted by default. To extend the list of allowed calls
/// configure the [`lint.flake8-boolean-trap.extend-allowed-calls`] option.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller, and to future
@@ -32,6 +35,9 @@ use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap;
/// func(flag=True)
/// ```
///
/// ## Options
/// - `lint.flake8-boolean-trap.extend-allowed-calls`
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
@@ -46,7 +52,7 @@ impl Violation for BooleanPositionalValueInCall {
}
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, call: &ast::ExprCall) {
if allow_boolean_trap(call) {
if allow_boolean_trap(call, checker) {
return;
}
for arg in call

View File

@@ -0,0 +1,25 @@
//! Settings for the `flake8-boolean-trap` plugin.
use std::fmt;
use ruff_macros::CacheKey;
use crate::display_settings;
#[derive(Debug, CacheKey, Default)]
pub struct Settings {
pub extend_allowed_calls: Vec<String>,
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_boolean_trap",
fields = [
self.extend_allowed_calls | array,
]
}
Ok(())
}
}

View File

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

View File

@@ -1,37 +1,61 @@
---
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:42:11: FBT003 Boolean positional value in function call
FBT.py:41:11: FBT003 Boolean positional value in function call
|
42 | used("a", True)
41 | used("a", True)
| ^^^^ FBT003
43 | used(do=True)
42 | used(do=True)
|
FBT.py:57:11: FBT003 Boolean positional value in function call
FBT.py:56:11: FBT003 Boolean positional value in function call
|
55 | {}.pop(True, False)
56 | dict.fromkeys(("world",), True)
57 | {}.deploy(True, False)
54 | {}.pop(True, False)
55 | dict.fromkeys(("world",), True)
56 | {}.deploy(True, False)
| ^^^^ FBT003
58 | getattr(someobj, attrname, False)
59 | mylist.index(True)
57 | getattr(someobj, attrname, False)
58 | mylist.index(True)
|
FBT.py:57:17: FBT003 Boolean positional value in function call
FBT.py:56:17: FBT003 Boolean positional value in function call
|
55 | {}.pop(True, False)
56 | dict.fromkeys(("world",), True)
57 | {}.deploy(True, False)
54 | {}.pop(True, False)
55 | dict.fromkeys(("world",), True)
56 | {}.deploy(True, False)
| ^^^^^ FBT003
58 | getattr(someobj, attrname, False)
59 | mylist.index(True)
57 | getattr(someobj, attrname, False)
58 | mylist.index(True)
|
FBT.py:121:10: FBT003 Boolean positional value in function call
FBT.py:120:10: FBT003 Boolean positional value in function call
|
121 | settings(True)
120 | settings(True)
| ^^^^ FBT003
|
FBT.py:144:20: FBT003 Boolean positional value in function call
|
142 | is_foo_or_bar=Case(
143 | When(Q(is_foo=True) | Q(is_bar=True)),
144 | then=Value(True),
| ^^^^ FBT003
145 | ),
146 | default=Value(False),
|
FBT.py:146:19: FBT003 Boolean positional value in function call
|
144 | then=Value(True),
145 | ),
146 | default=Value(False),
| ^^^^^ FBT003
147 | )
|
FBT.py:156:23: FBT003 Boolean positional value in function call
|
155 | class Settings(BaseSettings):
156 | foo: bool = Field(True, exclude=True)
| ^^^^ FBT003
|

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:41:11: FBT003 Boolean positional value in function call
|
41 | used("a", True)
| ^^^^ FBT003
42 | used(do=True)
|
FBT.py:56:11: FBT003 Boolean positional value in function call
|
54 | {}.pop(True, False)
55 | dict.fromkeys(("world",), True)
56 | {}.deploy(True, False)
| ^^^^ FBT003
57 | getattr(someobj, attrname, False)
58 | mylist.index(True)
|
FBT.py:56:17: FBT003 Boolean positional value in function call
|
54 | {}.pop(True, False)
55 | dict.fromkeys(("world",), True)
56 | {}.deploy(True, False)
| ^^^^^ FBT003
57 | getattr(someobj, attrname, False)
58 | mylist.index(True)
|
FBT.py:120:10: FBT003 Boolean positional value in function call
|
120 | settings(True)
| ^^^^ FBT003
|

View File

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

View File

@@ -100,6 +100,16 @@ impl<'a> GroupNameFinder<'a> {
self.usage_count += value;
}
}
/// Reset the usage count for the group name by the given value.
/// This function is called when there is a `continue`, `break`, or `return` statement.
fn reset_usage_count(&mut self) {
if let Some(last) = self.counter_stack.last_mut() {
*last.last_mut().unwrap() = 0;
} else {
self.usage_count = 0;
}
}
}
impl<'a> Visitor<'a> for GroupNameFinder<'a> {
@@ -197,6 +207,15 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> {
self.visit_expr(expr);
}
}
Stmt::Continue(_) | Stmt::Break(_) => {
self.reset_usage_count();
}
Stmt::Return(ast::StmtReturn { value, range: _ }) => {
if let Some(expr) = value {
self.visit_expr(expr);
}
self.reset_usage_count();
}
_ => visitor::walk_stmt(self, stmt),
}
}

View File

@@ -25,6 +25,26 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// ```python
/// foo = 1 + 1
/// ```
///
/// ## Known problems
/// This rule ignores expression types that are commonly used for their side
/// effects, such as function calls.
///
/// However, if a seemingly useless expression (like an attribute access) is
/// needed to trigger a side effect, consider assigning it to an anonymous
/// variable, to indicate that the return value is intentionally ignored.
///
/// For example, given:
/// ```python
/// with errors.ExceptionRaisedContext():
/// obj.attribute
/// ```
///
/// Use instead:
/// ```python
/// with errors.ExceptionRaisedContext():
/// _ = obj.attribute
/// ```
#[violation]
pub struct UselessExpression {
kind: Kind,

View File

@@ -195,4 +195,31 @@ B031.py:144:33: B031 Using the generator returned from `itertools.groupby()` mor
146 | for group in groupby(items, key=lambda p: p[1]):
|
B031.py:200:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
198 | if _section == "greens":
199 | collect_shop_items(shopper, section_items)
200 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
201 | return
|
B031.py:210:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
208 | elif _section == "frozen items":
209 | collect_shop_items(shopper, section_items)
210 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
211 |
212 | # Should trigger, since only one branch has a return statement.
|
B031.py:219:33: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
217 | elif _section == "frozen items":
218 | collect_shop_items(shopper, section_items)
219 | collect_shop_items(shopper, section_items) # B031
| ^^^^^^^^^^^^^ B031
220 |
221 | # Let's redefine the `groupby` function to make sure we pick up the correct one.
|

View File

@@ -793,7 +793,7 @@ pub(crate) fn fix_unnecessary_map(
}
/// (C419) Convert `[i for i in a]` into `i for i in a`
pub(crate) fn fix_unnecessary_comprehension_any_all(
pub(crate) fn fix_unnecessary_comprehension_in_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,

View File

@@ -12,13 +12,15 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
#[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))]
#[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))]
#[test_case(Rule::UnnecessaryComprehension, Path::new("C416.py"))]
#[test_case(Rule::UnnecessaryComprehensionAnyAll, Path::new("C419.py"))]
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419.py"))]
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_2.py"))]
#[test_case(Rule::UnnecessaryDoubleCastOrProcess, Path::new("C414.py"))]
#[test_case(Rule::UnnecessaryGeneratorDict, Path::new("C402.py"))]
#[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"))]
@@ -43,6 +45,24 @@ mod tests {
Ok(())
}
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_1.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_comprehensions").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))]
fn allow_dict_calls_with_keyword_arguments(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -1,7 +1,7 @@
pub(crate) use unnecessary_call_around_sorted::*;
pub(crate) use unnecessary_collection_call::*;
pub(crate) use unnecessary_comprehension::*;
pub(crate) use unnecessary_comprehension_any_all::*;
pub(crate) use unnecessary_comprehension_in_call::*;
pub(crate) use unnecessary_double_cast_or_process::*;
pub(crate) use unnecessary_generator_dict::*;
pub(crate) use unnecessary_generator_list::*;
@@ -21,7 +21,7 @@ mod helpers;
mod unnecessary_call_around_sorted;
mod unnecessary_collection_call;
mod unnecessary_comprehension;
mod unnecessary_comprehension_any_all;
mod unnecessary_comprehension_in_call;
mod unnecessary_double_cast_or_process;
mod unnecessary_generator_dict;
mod unnecessary_generator_list;

View File

@@ -11,15 +11,18 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
/// ## What it does
/// Checks for unnecessary list comprehensions passed to `any` and `all`.
/// Checks for unnecessary list comprehensions passed to builtin functions that take an iterable.
///
/// ## Why is this bad?
/// `any` and `all` take any iterators, including generators. Converting a generator to a list
/// by way of a list comprehension is unnecessary and reduces performance due to the
/// overhead of creating the list.
/// Many builtin functions (this rule currently covers `any`, `all`, `min`, `max`, and `sum`) take
/// any iterable, including a generator. Constructing a temporary list via list comprehension is
/// unnecessary and wastes memory for large iterables.
///
/// For example, compare the performance of `all` with a list comprehension against that
/// of a generator (~40x faster here):
/// `any` and `all` can also short-circuit iteration, saving a lot of time. The unnecessary
/// comprehension forces a full iteration of the input iterable, giving up the benefits of
/// short-circuiting. For example, compare the performance of `all` with a list comprehension
/// against that of a generator in a case where an early short-circuit is possible (almost 40x
/// faster):
///
/// ```console
/// In [1]: %timeit all([i for i in range(1000)])
@@ -29,26 +32,41 @@ use crate::rules::flake8_comprehensions::fixes;
/// 212 ns ± 0.892 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
/// ```
///
/// This performance improvement is due to short-circuiting. If the entire iterable has to be
/// traversed, the comprehension version may even be a bit faster: list allocation overhead is not
/// necessarily greater than generator overhead.
///
/// Applying this rule simplifies the code and will usually save memory, but in the absence of
/// short-circuiting it may not improve performance. (It may even slightly regress performance,
/// though the difference will usually be small.)
///
/// ## Examples
/// ```python
/// any([x.id for x in bar])
/// all([x.id for x in bar])
/// sum([x.val for x in bar])
/// min([x.val for x in bar])
/// max([x.val for x in bar])
/// ```
///
/// Use instead:
/// ```python
/// any(x.id for x in bar)
/// all(x.id for x in bar)
/// sum(x.val for x in bar)
/// min(x.val for x in bar)
/// max(x.val for x in bar)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the comprehension. In most cases, though, comments will be
/// preserved.
/// This rule's fix is marked as unsafe, as it can change the behavior of the code if the iteration
/// has side effects (due to laziness and short-circuiting). The fix may also drop comments when
/// rewriting some comprehensions.
///
#[violation]
pub struct UnnecessaryComprehensionAnyAll;
pub struct UnnecessaryComprehensionInCall;
impl Violation for UnnecessaryComprehensionAnyAll {
impl Violation for UnnecessaryComprehensionInCall {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
@@ -62,7 +80,7 @@ impl Violation for UnnecessaryComprehensionAnyAll {
}
/// C419
pub(crate) fn unnecessary_comprehension_any_all(
pub(crate) fn unnecessary_comprehension_in_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
@@ -72,10 +90,13 @@ pub(crate) fn unnecessary_comprehension_any_all(
if !keywords.is_empty() {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if !matches!(id.as_str(), "all" | "any") {
if !(matches!(id.as_str(), "any" | "all")
|| (checker.settings.preview.is_enabled() && matches!(id.as_str(), "sum" | "min" | "max")))
{
return;
}
let [arg] = args else {
@@ -93,9 +114,9 @@ pub(crate) fn unnecessary_comprehension_any_all(
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionInCall, arg.range());
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(expr, checker.locator(), checker.stylist())
fixes::fix_unnecessary_comprehension_in_call(expr, checker.locator(), checker.stylist())
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,8 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::ExprGenerator;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@@ -10,37 +12,53 @@ use super::helpers;
/// ## What it does
/// Checks for unnecessary generators that can be rewritten as `set`
/// comprehensions.
/// comprehensions (or with `set` directly).
///
/// ## Why is this bad?
/// It is unnecessary to use `set` around a generator expression, since
/// there are equivalent comprehensions for these types. Using a
/// comprehension is clearer and more idiomatic.
///
/// Further, if the comprehension can be removed entirely, as in the case of
/// `set(x for x in foo)`, it's better to use `set(foo)` directly, since it's
/// even more direct.
///
/// ## Examples
/// ```python
/// set(f(x) for x in foo)
/// set(x for x in foo)
/// ```
///
/// Use instead:
/// ```python
/// {f(x) for x in foo}
/// set(foo)
/// ```
///
/// ## 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.
#[violation]
pub struct UnnecessaryGeneratorSet;
pub struct UnnecessaryGeneratorSet {
short_circuit: bool,
}
impl AlwaysFixableViolation for UnnecessaryGeneratorSet {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary generator (rewrite as a `set` comprehension)")
if self.short_circuit {
format!("Unnecessary generator (rewrite using `set()`")
} else {
format!("Unnecessary generator (rewrite as a `set` comprehension)")
}
}
fn fix_title(&self) -> String {
"Rewrite as a `set` comprehension".to_string()
if self.short_circuit {
"Rewrite using `set()`".to_string()
} else {
"Rewrite as a `set` comprehension".to_string()
}
}
}
@@ -57,28 +75,59 @@ pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprC
if !checker.semantic().is_builtin("set") {
return;
}
if argument.is_generator_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),
);
let Some(ExprGenerator {
elt, generators, ..
}) = argument.as_generator_expr()
else {
return;
};
// 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);
// Short-circuit: given `set(x for x in y)`, generate `set(y)` (in lieu of `{x for x in y}`).
if let [generator] = generators.as_slice() {
if generator.ifs.is_empty() && !generator.is_async {
if ComparableExpr::from(elt) == ComparableExpr::from(&generator.target) {
let mut diagnostic = Diagnostic::new(
UnnecessaryGeneratorSet {
short_circuit: true,
},
call.range(),
);
let iterator = format!("set({})", checker.locator().slice(generator.iter.range()));
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
iterator,
call.range(),
)));
checker.diagnostics.push(diagnostic);
return;
}
}
}
// Convert `set(f(x) for x in y)` to `{f(x) for x in y}`.
let mut diagnostic = Diagnostic::new(
UnnecessaryGeneratorSet {
short_circuit: false,
},
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(),
);
Fix::unsafe_edits(call_start, [call_end])
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,255 +1,249 @@
---
source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
---
C401.py:1:5: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:2:13: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
1 | x = set(x for x in range(3))
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
2 | x = set(x for x in range(3))
3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
1 | # Cannot conbime with C416. Should use set comprehension here.
2 | even_nums = set(2 * x for x in range(3))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
3 | odd_nums = set(
4 | 2 * x + 1 for x in range(3)
|
= help: Rewrite as a `set` comprehension
Unsafe fix
1 |-x = set(x for x in range(3))
1 |+x = {x for x in range(3)}
2 2 | x = set(x for x in range(3))
3 3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
1 1 | # Cannot conbime with C416. Should use set comprehension here.
2 |-even_nums = set(2 * x for x in range(3))
2 |+even_nums = {2 * x for x in range(3)}
3 3 | odd_nums = set(
4 4 | 2 * x + 1 for x in range(3)
5 5 | )
C401.py:2:5: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:3:12: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
1 | x = set(x for x in range(3))
2 | x = set(x for x in range(3))
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
1 | # Cannot conbime with C416. Should use set comprehension here.
2 | even_nums = set(2 * x for x in range(3))
3 | odd_nums = set(
| ____________^
4 | | 2 * x + 1 for x in range(3)
5 | | )
| |_^ C401
6 | small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
|
= help: Rewrite as a `set` comprehension
Unsafe fix
1 1 | x = set(x for x in range(3))
2 |-x = set(x for x in range(3))
2 |+x = {x for x in range(3)}
3 3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5 5 | print(f"Hello {set(a for a in range(3))} World")
C401.py:3:8: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
1 | x = set(x for x in range(3))
2 | x = set(x for x in range(3))
3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5 | print(f"Hello {set(a for a in range(3))} World")
|
= help: Rewrite as a `set` comprehension
Unsafe fix
1 1 | x = set(x for x in range(3))
2 2 | x = set(x for x in range(3))
3 |-y = f"{set(a if a < 6 else 0 for a in range(3))}"
3 |+y = f"{ {a if a < 6 else 0 for a in range(3)} }"
4 4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5 5 | print(f"Hello {set(a for a in range(3))} World")
6 6 |
C401.py:4:17: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
2 | x = set(x for x in range(3))
3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
5 | print(f"Hello {set(a for a in range(3))} World")
|
= help: Rewrite as a `set` comprehension
Unsafe fix
1 1 | x = set(x for x in range(3))
2 2 | x = set(x for x in range(3))
3 3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 |-_ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
4 |+_ = "{}".format({a if a < 6 else 0 for a in range(3)})
5 5 | print(f"Hello {set(a for a in range(3))} World")
6 6 |
7 7 |
C401.py:5:16: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5 | print(f"Hello {set(a for a in range(3))} World")
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
|
= help: Rewrite as a `set` comprehension
Unsafe fix
2 2 | x = set(x for x in range(3))
3 3 | y = f"{set(a if a < 6 else 0 for a in range(3))}"
4 4 | _ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5 |-print(f"Hello {set(a for a in range(3))} World")
5 |+print(f"Hello { {a for a in range(3)} } World")
6 6 |
1 1 | # Cannot conbime with C416. Should use set comprehension here.
2 2 | even_nums = set(2 * x for x in range(3))
3 |-odd_nums = set(
3 |+odd_nums = {
4 4 | 2 * x + 1 for x in range(3)
5 |-)
5 |+}
6 6 | small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
7 7 |
8 8 | def f(x):
C401.py:12:16: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:6:17: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
4 | 2 * x + 1 for x in range(3)
5 | )
6 | small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
7 |
8 | def f(x):
|
= help: Rewrite as a `set` comprehension
Unsafe fix
3 3 | odd_nums = set(
4 4 | 2 * x + 1 for x in range(3)
5 5 | )
6 |-small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
6 |+small_nums = f"{ {a if a < 6 else 0 for a in range(3)} }"
7 7 |
8 8 | def f(x):
9 9 | return x
C401.py:11:16: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
12 | print(f'Hello {set(a for a in "abc")} World')
| ^^^^^^^^^^^^^^^^^^^^^ C401
13 | print(f"Hello {set(a for a in 'abc')} World")
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
9 | return x
10 |
11 | print(f"Hello {set(f(a) for a in 'abc')} World")
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
12 | print(f"Hello { set(f(a) for a in 'abc') } World")
|
= help: Rewrite as a `set` comprehension
Unsafe fix
8 8 | def f(x):
9 9 | return x
10 10 |
11 |-print(f"Hello {set(f(a) for a in 'abc')} World")
11 |+print(f"Hello { {f(a) for a in 'abc'} } World")
12 12 | print(f"Hello { set(f(a) for a in 'abc') } World")
13 13 |
14 14 |
C401.py:12:17: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
11 | print(f"Hello {set(f(a) for a in 'abc')} World")
12 | print(f"Hello { set(f(a) for a in 'abc') } World")
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
|
= help: Rewrite as a `set` comprehension
Unsafe fix
9 9 | return x
10 10 |
11 11 |
12 |-print(f'Hello {set(a for a in "abc")} World')
12 |+print(f'Hello { {a for a in "abc"} } World')
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
11 11 | print(f"Hello {set(f(a) for a in 'abc')} World")
12 |-print(f"Hello { set(f(a) for a in 'abc') } World")
12 |+print(f"Hello { {f(a) for a in 'abc'} } World")
13 13 |
14 14 |
15 15 | # Short-circuit case, combine with C416 and should produce x = set(range(3))
C401.py:13:16: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:16:5: C401 [*] Unnecessary generator (rewrite using `set()`
|
12 | print(f'Hello {set(a for a in "abc")} World')
13 | print(f"Hello {set(a for a in 'abc')} World")
| ^^^^^^^^^^^^^^^^^^^^^ C401
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
15 | # Short-circuit case, combine with C416 and should produce x = set(range(3))
16 | x = set(x for x in range(3))
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
17 | x = set(
18 | x for x in range(3)
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
10 10 |
11 11 |
12 12 | print(f'Hello {set(a for a in "abc")} World')
13 |-print(f"Hello {set(a for a in 'abc')} World")
13 |+print(f"Hello { {a for a in 'abc'} } World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
13 13 |
14 14 |
15 15 | # Short-circuit case, combine with C416 and should produce x = set(range(3))
16 |-x = set(x for x in range(3))
16 |+x = set(range(3))
17 17 | x = set(
18 18 | x for x in range(3)
19 19 | )
C401.py:14:16: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:17:5: C401 [*] Unnecessary generator (rewrite using `set()`
|
12 | print(f'Hello {set(a for a in "abc")} World')
13 | print(f"Hello {set(a for a in 'abc')} World")
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | # Short-circuit case, combine with C416 and should produce x = set(range(3))
16 | x = set(x for x in range(3))
17 | x = set(
| _____^
18 | | x for x in range(3)
19 | | )
| |_^ C401
20 | print(f"Hello {set(a for a in range(3))} World")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
|
= help: Rewrite using `set()`
Unsafe fix
14 14 |
15 15 | # Short-circuit case, combine with C416 and should produce x = set(range(3))
16 16 | x = set(x for x in range(3))
17 |-x = set(
18 |- x for x in range(3)
19 |-)
17 |+x = set(range(3))
20 18 | print(f"Hello {set(a for a in range(3))} World")
21 19 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 20 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
C401.py:20:16: C401 [*] Unnecessary generator (rewrite using `set()`
|
18 | x for x in range(3)
19 | )
20 | print(f"Hello {set(a for a in range(3))} World")
| ^^^^^^^^^^^^^^^^^^^^^^^^ C401
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
11 11 |
12 12 | print(f'Hello {set(a for a in "abc")} World')
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 |-print(f"Hello {set(f(a) for a in 'abc')} World")
14 |+print(f"Hello { {f(a) for a in 'abc'} } World")
15 15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
17 17 |
17 17 | x = set(
18 18 | x for x in range(3)
19 19 | )
20 |-print(f"Hello {set(a for a in range(3))} World")
20 |+print(f"Hello {set(range(3))} World")
21 21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
23 23 |
C401.py:15:10: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:21:10: C401 [*] Unnecessary generator (rewrite using `set()`
|
13 | print(f"Hello {set(a for a in 'abc')} World")
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
19 | )
20 | print(f"Hello {set(a for a in range(3))} World")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
| ^^^^^^^^^^^^^^^^^^^^^ C401
16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
12 12 | print(f'Hello {set(a for a in "abc")} World')
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 |-print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
15 |+print(f"{ {a for a in 'abc'} - set(a for a in 'ab')}")
16 16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
17 17 |
18 18 | # The fix generated for this diagnostic is incorrect, as we add additional space
18 18 | x for x in range(3)
19 19 | )
20 20 | print(f"Hello {set(a for a in range(3))} World")
21 |-print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
21 |+print(f"{set('abc') - set(a for a in 'ab')}")
22 22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
23 23 |
24 24 |
C401.py:15:34: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:21:34: C401 [*] Unnecessary generator (rewrite using `set()`
|
13 | print(f"Hello {set(a for a in 'abc')} World")
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
19 | )
20 | print(f"Hello {set(a for a in range(3))} World")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
| ^^^^^^^^^^^^^^^^^^^^ C401
16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
12 12 | print(f'Hello {set(a for a in "abc")} World')
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 |-print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
15 |+print(f"{set(a for a in 'abc') - {a for a in 'ab'} }")
16 16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
17 17 |
18 18 | # The fix generated for this diagnostic is incorrect, as we add additional space
18 18 | x for x in range(3)
19 19 | )
20 20 | print(f"Hello {set(a for a in range(3))} World")
21 |-print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
21 |+print(f"{set(a for a in 'abc') - set('ab')}")
22 22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
23 23 |
24 24 |
C401.py:16:11: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:22:11: C401 [*] Unnecessary generator (rewrite using `set()`
|
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
20 | print(f"Hello {set(a for a in range(3))} World")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
| ^^^^^^^^^^^^^^^^^^^^^ C401
17 |
18 | # The fix generated for this diagnostic is incorrect, as we add additional space
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 |-print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
16 |+print(f"{ {a for a in 'abc'} - set(a for a in 'ab') }")
17 17 |
18 18 | # The fix generated for this diagnostic is incorrect, as we add additional space
19 19 | # around the set comprehension.
19 19 | )
20 20 | print(f"Hello {set(a for a in range(3))} World")
21 21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 |-print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
22 |+print(f"{ set('abc') - set(a for a in 'ab') }")
23 23 |
24 24 |
25 25 | # Not built-in set.
C401.py:16:35: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
C401.py:22:35: C401 [*] Unnecessary generator (rewrite using `set()`
|
14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
20 | print(f"Hello {set(a for a in range(3))} World")
21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 | print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
| ^^^^^^^^^^^^^^^^^^^^ C401
17 |
18 | # The fix generated for this diagnostic is incorrect, as we add additional space
|
= help: Rewrite as a `set` comprehension
= help: Rewrite using `set()`
Unsafe fix
13 13 | print(f"Hello {set(a for a in 'abc')} World")
14 14 | print(f"Hello {set(f(a) for a in 'abc')} World")
15 15 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
16 |-print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
16 |+print(f"{ set(a for a in 'abc') - {a for a in 'ab'} }")
17 17 |
18 18 | # The fix generated for this diagnostic is incorrect, as we add additional space
19 19 | # around the set comprehension.
C401.py:20:12: C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
|
18 | # The fix generated for this diagnostic is incorrect, as we add additional space
19 | # around the set comprehension.
20 | print(f"{ {set(a for a in 'abc')} }")
| ^^^^^^^^^^^^^^^^^^^^^ C401
|
= help: Rewrite as a `set` comprehension
Unsafe fix
17 17 |
18 18 | # The fix generated for this diagnostic is incorrect, as we add additional space
19 19 | # around the set comprehension.
20 |-print(f"{ {set(a for a in 'abc')} }")
20 |+print(f"{ { {a for a in 'abc'} } }")
19 19 | )
20 20 | print(f"Hello {set(a for a in range(3))} World")
21 21 | print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
22 |-print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
22 |+print(f"{ set(a for a in 'abc') - set('ab') }")
23 23 |
24 24 |
25 25 | # Not built-in set.

View File

@@ -98,67 +98,65 @@ C419.py:9:5: C419 [*] Unnecessary list comprehension
11 11 | # OK
12 12 | all(x.id for x in bar)
C419.py:24:5: C419 [*] Unnecessary list comprehension
C419.py:28:5: C419 [*] Unnecessary list comprehension
|
22 | # Special comment handling
23 | any(
24 | [ # lbracket comment
26 | # Special comment handling
27 | any(
28 | [ # lbracket comment
| _____^
25 | | # second line comment
26 | | i.bit_count()
27 | | # random middle comment
28 | | for i in range(5) # rbracket comment
29 | | ] # rpar comment
29 | | # second line comment
30 | | i.bit_count()
31 | | # random middle comment
32 | | for i in range(5) # rbracket comment
33 | | ] # rpar comment
| |_____^ C419
30 | # trailing comment
31 | )
34 | # trailing comment
35 | )
|
= help: Remove unnecessary list comprehension
Unsafe fix
21 21 |
22 22 | # Special comment handling
23 23 | any(
24 |- [ # lbracket comment
25 |- # second line comment
26 |- i.bit_count()
24 |+ # lbracket comment
25 |+ # second line comment
26 |+ i.bit_count()
27 27 | # random middle comment
28 |- for i in range(5) # rbracket comment
29 |- ] # rpar comment
28 |+ for i in range(5) # rbracket comment # rpar comment
30 29 | # trailing comment
31 30 | )
32 31 |
25 25 |
26 26 | # Special comment handling
27 27 | any(
28 |- [ # lbracket comment
29 |- # second line comment
30 |- i.bit_count()
28 |+ # lbracket comment
29 |+ # second line comment
30 |+ i.bit_count()
31 31 | # random middle comment
32 |- for i in range(5) # rbracket comment
33 |- ] # rpar comment
32 |+ for i in range(5) # rbracket comment # rpar comment
34 33 | # trailing comment
35 34 | )
36 35 |
C419.py:35:5: C419 [*] Unnecessary list comprehension
C419.py:39:5: C419 [*] Unnecessary list comprehension
|
33 | # Weird case where the function call, opening bracket, and comment are all
34 | # on the same line.
35 | any([ # lbracket comment
37 | # Weird case where the function call, opening bracket, and comment are all
38 | # on the same line.
39 | any([ # lbracket comment
| _____^
36 | | # second line comment
37 | | i.bit_count() for i in range(5) # rbracket comment
38 | | ] # rpar comment
40 | | # second line comment
41 | | i.bit_count() for i in range(5) # rbracket comment
42 | | ] # rpar comment
| |_____^ C419
39 | )
43 | )
|
= help: Remove unnecessary list comprehension
Unsafe fix
32 32 |
33 33 | # Weird case where the function call, opening bracket, and comment are all
34 34 | # on the same line.
35 |-any([ # lbracket comment
36 |- # second line comment
37 |- i.bit_count() for i in range(5) # rbracket comment
38 |- ] # rpar comment
35 |+any(
36 |+# lbracket comment
37 |+# second line comment
38 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment
39 39 | )
36 36 |
37 37 | # Weird case where the function call, opening bracket, and comment are all
38 38 | # on the same line.
39 |-any([ # lbracket comment
40 |- # second line comment
41 |- i.bit_count() for i in range(5) # rbracket comment
42 |- ] # rpar comment
39 |+any(
40 |+# lbracket comment
41 |+# second line comment
42 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment
43 43 | )

View File

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

View File

@@ -0,0 +1,55 @@
---
source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
---
C419_1.py:1:5: C419 [*] Unnecessary list comprehension
|
1 | sum([x.val for x in bar])
| ^^^^^^^^^^^^^^^^^^^^ C419
2 | min([x.val for x in bar])
3 | max([x.val for x in bar])
|
= help: Remove unnecessary list comprehension
Unsafe fix
1 |-sum([x.val for x in bar])
1 |+sum(x.val for x in bar)
2 2 | min([x.val for x in bar])
3 3 | max([x.val for x in bar])
4 4 |
C419_1.py:2:5: C419 [*] Unnecessary list comprehension
|
1 | sum([x.val for x in bar])
2 | min([x.val for x in bar])
| ^^^^^^^^^^^^^^^^^^^^ C419
3 | max([x.val for x in bar])
|
= help: Remove unnecessary list comprehension
Unsafe fix
1 1 | sum([x.val for x in bar])
2 |-min([x.val for x in bar])
2 |+min(x.val for x in bar)
3 3 | max([x.val for x in bar])
4 4 |
5 5 | # Ok
C419_1.py:3:5: C419 [*] Unnecessary list comprehension
|
1 | sum([x.val for x in bar])
2 | min([x.val for x in bar])
3 | max([x.val for x in bar])
| ^^^^^^^^^^^^^^^^^^^^ C419
4 |
5 | # Ok
|
= help: Remove unnecessary list comprehension
Unsafe fix
1 1 | sum([x.val for x in bar])
2 2 | min([x.val for x in bar])
3 |-max([x.val for x in bar])
3 |+max(x.val for x in bar)
4 4 |
5 5 | # Ok
6 6 | sum(x.val for x in bar)

View File

@@ -71,6 +71,20 @@ import os
r"
# Copyright (C) 2021-2023
import os
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
assert_messages!(diagnostics);
}
#[test]
fn notice_with_comma() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2021, 2022
import os
"
.trim(),
@@ -85,6 +99,126 @@ import os
r"
# Copyright (C) 2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_dash() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022-2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_dash_invalid_space() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022- 2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_dash_invalid_spaces() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022 - 2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_comma_invalid_no_space() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022,2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_comma_invalid_spaces() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022 , 2023 Ruff
import os
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice])
},
);
assert_messages!(diagnostics);
}
#[test]
fn valid_author_with_comma_valid_space() {
let diagnostics = test_snippet(
r"
# Copyright (C) 2022, 2023 Ruff
import os
"
.trim(),

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