Compare commits

...

65 Commits

Author SHA1 Message Date
Micha Reiser
b53118ed00 Bump version to v0.3.0 (#10151)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-02-29 16:05:20 +01:00
Micha Reiser
52f4c1e41b Remove deprecated CLI option --format (#10170)
Co-authored-by: Tibor Reiss <tibor.reiss@gmail.com>
2024-02-29 13:59:08 +00:00
Micha Reiser
eceffe74a0 Deprecate ruff <path> ruff --explain, ruff --clean and ruff --generate-shell-completion (#10169) 2024-02-29 14:50:01 +01:00
Justin Sexton
c73c497477 [pydocstyle] Trim whitespace when removing blank lines after section (D413) (#10162)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-29 13:29:40 +00:00
Hoël Bagard
c9c98c4fe3 Fix mkdocs local link (#10167) 2024-02-29 11:35:10 +01:00
Micha Reiser
72ccb34ba6 Fix ecosystem check for indico (#10164) 2024-02-29 10:27:33 +01:00
Micha Reiser
dcc92f50cf Update black tests (#10166) 2024-02-29 10:00:51 +01:00
Micha Reiser
a6f32ddc5e Ruff 2024.2 style (#9639) 2024-02-29 09:30:54 +01:00
Jane Lewis
0293908b71 Implement RUF028 to detect useless formatter suppression comments (#9899)
<!--
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?
-->
Fixes #6611

## Summary

This lint rule spots comments that are _intended_ to suppress or enable
the formatter, but will be ignored by the Ruff formatter.

We borrow some functions the formatter uses for determining comment
placement / putting them in context within an AST.

The analysis function uses an AST visitor to visit each comment and
attach it to the AST. It then uses that context to check:
1. Is this comment in an expression?
2. Does this comment have bad placement? (e.g. a `# fmt: skip` above a
function instead of at the end of a line)
3. Is this comment redundant?
4. Does this comment actually suppress any code?
5. Does this comment have ambiguous placement? (e.g. a `# fmt: off`
above an `else:` block)

If any of these are true, a violation is thrown. The reported reason
depends on the order of the above check-list: in other words, a `# fmt:
skip` comment on its own line within a list expression will be reported
as being in an expression, since that reason takes priority.

The lint suggests removing the comment as an unsafe fix, regardless of
the reason.

## Test Plan

A snapshot test has been created.
2024-02-28 19:21:06 +00:00
Philipp Thiel
36bc725eaa [flake8-bugbear] Avoid adding default initializers to stubs (B006) (#10152)
## Summary

Adapts the fix for rule B006 to no longer modify the body of function
stubs, while retaining the change in method signature.

## Test Plan

The existing tests for B006 were adapted to reflect this change in
behavior.

## Relevant issue

https://github.com/astral-sh/ruff/issues/10083
2024-02-28 18:19:36 +00:00
Alex Waygood
8f92da8b6c Fix the ecosystem check (#10155) 2024-02-28 17:42:06 +00:00
Charlie Marsh
a1905172a8 [flake8-bandit] Remove suspicious-lxml-import (S410) (#10154)
## Summary

The `lxml` library has been modified to address known vulnerabilities
and unsafe defaults. As such, the `defusedxml`
library is no longer necessary, `defusedxml` has deprecated its `lxml`
module.

Closes https://github.com/astral-sh/ruff/issues/10030.
2024-02-28 12:38:55 -05:00
Micha Reiser
1791e7d73b Limit isort.lines-after-imports to 1 for stub files (#9971) 2024-02-28 17:36:51 +01:00
Charlie Marsh
317d2e4c75 Remove build from the default exclusion list (#10093)
## Summary

This is a not-unpopular directory name, and it's led to tons of issues
and user confusion (most recently:
https://github.com/astral-sh/ruff-pre-commit/issues/69). I've wanted to
remove it for a long time, but we need to do so as part of a minor
release.
2024-02-28 16:30:38 +00:00
Micha Reiser
8044c24c7e Remove "Beta" Label from formatter documentation (#10144) 2024-02-28 12:47:37 +00:00
Robin Caloudis
a1e8784207 [ruff] Expand rule for list(iterable).pop(0) idiom (RUF015) (#10148)
## Summary

Currently, rule `RUF015` is not able to detect the usage of
`list(iterable).pop(0)` falling under the category of an _unnecessary
iterable allocation for accessing the first element_. This PR wants to
change that. See the underlying issue for more details.

* Provide extension to detect `list(iterable).pop(0)`, but not
`list(iterable).pop(i)` where i > 1
* Update corresponding doc

## Test Plan

* `RUF015.py` and the corresponding snap file were extended such that
their correspond to the new behaviour

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

--- 

PS: I've only been working on this ticket as I haven't seen any activity
from issue assignee @rmad17, neither in this repo nor in a fork. I hope
I interpreted his inactivity correctly. Didn't mean to steal his chance.
Since I stumbled across the underlying problem myself, I wanted to offer
a solution as soon as possible.
2024-02-28 00:24:28 +00:00
Micha Reiser
8dc22d5793 Perf: Skip string normalization when possible (#10116) 2024-02-26 17:35:29 +00:00
Micha Reiser
15b87ea8be E203: Don't warn about single whitespace before tuple , (#10094) 2024-02-26 18:22:35 +01:00
Alex Waygood
c25f1cd12a Explicitly ban overriding extend as part of a --config flag (#10135) 2024-02-26 16:07:28 +00:00
Charlie Marsh
f5904a20d5 Add package name to requirements-insiders.txt (#10090) 2024-02-26 10:52:58 -05:00
Micha Reiser
77c5561646 Add parenthesized flag to ExprTuple and ExprGenerator (#9614) 2024-02-26 15:35:20 +00:00
Arkin Modi
ab4bd71755 docs: fix pycodestyle.max-line-length link (#10136) 2024-02-26 14:58:13 +01:00
Micha Reiser
5abf662365 Upgrade codspeed-criterion for perf boost (#10134) 2024-02-26 11:37:23 +00:00
Alex Waygood
14fa1c5b52 [Minor] Improve the style of some tests in crates/ruff/tests/format.rs (#10132) 2024-02-26 11:02:09 +00:00
Alex Waygood
0421c41ff7 Add Alex Waygood as a CODEOWNER for flake8-pyi (#10129) 2024-02-26 10:25:46 +00:00
dependabot[bot]
ad4695d3eb Bump serde from 1.0.196 to 1.0.197 (#10128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:26:00 +00:00
dependabot[bot]
8c58ebee37 Bump insta from 1.34.0 to 1.35.1 (#10127)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:15:13 +00:00
dependabot[bot]
5023874355 Bump bstr from 1.9.0 to 1.9.1 (#10124)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 10:14:22 +01:00
dependabot[bot]
5554510597 Bump syn from 2.0.49 to 2.0.51 (#10126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:14:00 +00:00
dependabot[bot]
1341e064a7 Bump serde-wasm-bindgen from 0.6.3 to 0.6.4 (#10125)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:13:15 +00:00
Micha Reiser
bd98d6884b Upgrade upload/download artifact actions in release workflow (#10105) 2024-02-26 08:23:10 +01:00
Micha Reiser
761d4d42f1 Add cold attribute to less likely printer queue branches (#10121) 2024-02-26 08:19:40 +01:00
Robin Caloudis
fc8738f52a [ruff] Avoid f-string false positives in gettext calls (RUF027) (#10118)
## Summary

It is a convention to use the `_()` alias for `gettext()`. We want to
avoid
statement expressions and assignments related to aliases of the gettext
API.
See https://docs.python.org/3/library/gettext.html for details. When one
uses `_() to mark a string for translation, the tools look for these
markers
and replace the original string with its translated counterpart. If the
string contains variable placeholders or formatting, it can complicate
the
translation process, lead to errors or incorrect translations.

## Test Plan

* Test file `RUF027_1.py` was extended such that the test reproduces the
false-positive

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-25 18:17:56 -05:00
Micha Reiser
1711bca4a0 FString formatting: remove fstring handling in normalize_string (#10119) 2024-02-25 18:28:46 +01:00
Raphael Boidol
51ce88bb23 docs: update actions in documentation to recent versions without deprecation warnings (#10109) 2024-02-24 13:16:10 +00:00
Micha Reiser
36d8b03b5f Upgrade upload/download artifact actions in CI.yaml workflow (#10104) 2024-02-23 21:36:28 +01:00
Micha Reiser
a284c711bf Refactor trailing comma rule into explicit check and state update code (#10100) 2024-02-23 17:56:05 +01:00
Micha Reiser
8c20f14e62 Set PowerPC Page Size to 64KB (#10080) 2024-02-23 08:32:21 +01:00
Charlie Marsh
946028e358 Respect runtime-required decorators for function signatures (#10091)
## Summary

The original implementation of this applied the runtime-required context
to definitions _within_ the function, but not the signature itself. (We
had test coverage; the snapshot was just correctly showing the wrong
outcome.)

Closes https://github.com/astral-sh/ruff/issues/10089.
2024-02-23 03:33:08 +00:00
Charlie Marsh
6fe15e7289 Allow © in copyright notices (#10065)
Closes https://github.com/astral-sh/ruff/issues/10061.
2024-02-22 12:44:22 -05:00
Seo Sanghyeon
7d9ce5049a PLR0203: Delete entire statement, including semicolons (#10074)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-22 16:03:00 +00:00
Vladimir Iglovikov
ecd5a7035d Added Image Augmentation library Albumentations (13k stars) to Readme to "Who is using Ruff" section (#10075) 2024-02-22 16:59:13 +01:00
Arjun Munji
175c266de3 Omit repeated equality comparison for sys (#10054)
## Summary
Update PLR1714 to ignore `sys.platform` and `sys.version` checks. 
I'm not sure if these checks or if we need to add more. Please advise.

Fixes #10017

## Test Plan
Added a new test case and ran `cargo nextest run`
2024-02-20 19:03:32 +00:00
Charlie Marsh
4997c681f1 [pycodestyle] Allow os.environ modifications between imports (E402) (#10066)
## Summary

Allows, e.g.:

```python
import os

os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")

import torch
```

For now, this is only allowed in preview.

Closes https://github.com/astral-sh/ruff/issues/10059
2024-02-20 13:24:27 -05:00
Seo Sanghyeon
7eafba2a4d [pyupgrade] Detect literals with unary operators (UP018) (#10060)
Fix #10029.
2024-02-20 18:21:06 +00:00
Ottavio Hartman
0f70c99c42 feat(ERA001): detect single-line code for try:, except:, etc. (#10057)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-20 18:40:18 +01:00
Micha Reiser
ee4efdba96 Fix ecosystem (#10064) 2024-02-20 16:54:50 +01:00
Ottavio Hartman
0d363ab239 fix(ERA001): detect commented out case statements, add more one-line support (#10055)
## Summary

Closes #10031 

- Detect commented out `case` statements. Playground repro:
https://play.ruff.rs/5a305aa9-6e5c-4fa4-999a-8fc427ab9a23
- Add more support for one-line commented out code.

## Test Plan

Unit tested and tested with
```sh
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/eradicate/ERA001.py --no-cache --preview --select ERA001
```

TODO:
- [x] `cargo insta test`
2024-02-19 22:56:42 -05:00
Daniël van Noord
68b8abf9c6 [pylint] Add PLE1141 DictIterMissingItems (#9845)
## Summary

References https://github.com/astral-sh/ruff/issues/970.

Implements
[`dict-iter-missing-items`](https://pylint.readthedocs.io/en/latest/user_guide/messages/error/dict-iter-missing-items.html).

Took the tests from "upstream"
[here](https://github.com/DanielNoord/pylint/blob/main/tests/functional/d/dict_iter_missing_items.py).

~I wasn't able to implement code for one false positive, but it is
pretty estoric: https://github.com/pylint-dev/pylint/issues/3283. I
would personally argue that adding this check as preview rule without
supporting this specific use case is fine. I did add a "test" for it.~
This was implemented.

## Test Plan

Followed the Contributing guide to create tests, hopefully I didn't miss
any.
Also ran CI on my own fork and seemed to be all okay 😄 

~Edit: the ecosystem check seems a bit all over the place? 😅~ All good.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-19 19:56:55 +05:30
Seo Sanghyeon
1c8851e5fb Do multiline string test for W293 too (#10049) 2024-02-19 11:58:56 +00:00
Micha Reiser
4ac19993cf Add Micha as owner to formatter and parser (#10048) 2024-02-19 10:26:12 +00:00
dependabot[bot]
5cd3c6ef07 Bump anyhow from 1.0.79 to 1.0.80 (#10043)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:42 +01:00
dependabot[bot]
e94a2615a8 Bump semver from 1.0.21 to 1.0.22 (#10044)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:31 +01:00
dependabot[bot]
77f577cba7 Bump syn from 2.0.48 to 2.0.49 (#10045)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:21 +01:00
dependabot[bot]
49a46c2880 Bump clap from 4.5.0 to 4.5.1 (#10046)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:11 +01:00
dependabot[bot]
67e17e2750 Bump ureq from 2.9.5 to 2.9.6 (#10047)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:37:50 +01:00
Charlie Marsh
e1928be36e Allow boolean positionals in __post_init__ (#10027)
Closes https://github.com/astral-sh/ruff/issues/10011.
2024-02-18 15:03:17 +00:00
Charlie Marsh
235cfb7976 Bump version to v0.2.2 (#10018) 2024-02-17 22:15:04 +00:00
Dhruv Manilawala
91ae81b565 Move RUF001, RUF002 to AST checker (#9993)
## Summary

Part of #7595 

This PR moves the `RUF001` and `RUF002` rules to the AST checker. This
removes the use of docstring detection from these rules.

## Test Plan

As this is just a refactor, make sure existing test cases pass.
2024-02-17 17:01:31 +00:00
Adam Kuhn
d46c5d8ac8 docs: Formatter compatibility warning for D207 and D300 (#10007)
- Update docs to mention formatter compatibility interactions for
under-indentation (D207) and triple-single-quotes (D300)
- Changes verified locally with mkdocs
- Closes: https://github.com/astral-sh/ruff/issues/9675
2024-02-17 07:37:38 -05:00
Jane Lewis
20217e9bbd Fix panic on RUF027 (#9990)
## Summary

Fixes #9895 

The cause for this panic came from an offset error in the code. When
analyzing a hypothetical f-string, we attempt to re-parse it as an
f-string, and use the AST data to determine, among other things, whether
the format specifiers are correct. To determine the 'correctness' of a
format specifier, we actually have to re-parse the format specifier, and
this is where the issue lies. To get the source text for the specifier,
we were taking a slice from the original file source text... even though
the AST data for the specifier belongs to the standalone parsed f-string
expression, meaning that the ranges are going to be way off. In a file
with Unicode, this can cause panics if the slice is inside a char
boundary.

To fix this, we now slice from the temporary source we created earlier
to parse the literal as an f-string.

## Test Plan

The RUF027 snapshot test was amended to include a string with format
specifiers which we _should_ be calling out. This is to ensure we do
slice format specifiers from the source text correctly.
2024-02-16 20:04:39 +00:00
Dhruv Manilawala
72bf1c2880 Preview minimal f-string formatting (#9642)
## Summary

_This is preview only feature and is available using the `--preview`
command-line flag._

With the implementation of [PEP 701] in Python 3.12, f-strings can now
be broken into multiple lines, can contain comments, and can re-use the
same quote character. Currently, no other Python formatter formats the
f-strings so there's some discussion which needs to happen in defining
the style used for f-string formatting. Relevant discussion:
https://github.com/astral-sh/ruff/discussions/9785

The goal for this PR is to add minimal support for f-string formatting.
This would be to format expression within the replacement field without
introducing any major style changes.

### Newlines

The heuristics for adding newline is similar to that of
[Prettier](https://prettier.io/docs/en/next/rationale.html#template-literals)
where the formatter would only split an expression in the replacement
field across multiple lines if there was already a line break within the
replacement field.

In other words, the formatter would not add any newlines unless they
were already present i.e., they were added by the user. This makes
breaking any expression inside an f-string optional and in control of
the user. For example,

```python
# We wouldn't break this
aaaaaaaaaaa = f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"

# But, we would break the following as there's already a newline
aaaaaaaaaaa = f"asaaaaaaaaaaaaaaaa {
	aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
```


If there are comments in any of the replacement field of the f-string,
then it will always be a multi-line f-string in which case the formatter
would prefer to break expressions i.e., introduce newlines. For example,

```python
x = f"{ # comment
    a }"
```

### Quotes

The logic for formatting quotes remains unchanged. The existing logic is
used to determine the necessary quote char and is used accordingly.

Now, if the expression inside an f-string is itself a string like, then
we need to make sure to preserve the existing quote and not change it to
the preferred quote unless it's 3.12. For example,

```python
f"outer {'inner'} outer"

# For pre 3.12, preserve the single quote
f"outer {'inner'} outer"

# While for 3.12 and later, the quotes can be changed
f"outer {"inner"} outer"
```

But, for triple-quoted strings, we can re-use the same quote char unless
the inner string is itself a triple-quoted string.

```python
f"""outer {"inner"} outer"""  # valid
f"""outer {'''inner'''} outer"""  # preserve the single quote char for the inner string
```

### Debug expressions

If debug expressions are present in the replacement field of a f-string,
then the whitespace needs to be preserved as they will be rendered as it
is (for example, `f"{ x = }"`. If there are any nested f-strings, then
the whitespace in them needs to be preserved as well which means that
we'll stop formatting the f-string as soon as we encounter a debug
expression.

```python
f"outer {   x =  !s  :.3f}"
#                  ^^
#                  We can remove these whitespaces
```

Now, the whitespace doesn't need to be preserved around conversion spec
and format specifiers, so we'll format them as usual but we won't be
formatting any nested f-string within the format specifier.

### Miscellaneous

- The
[`hug_parens_with_braces_and_square_brackets`](https://github.com/astral-sh/ruff/issues/8279)
preview style isn't implemented w.r.t. the f-string curly braces.
- The
[indentation](https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590)
is always relative to the f-string containing statement

## Test Plan

* Add new test cases
* Review existing snapshot changes
* Review the ecosystem changes

[PEP 701]: https://peps.python.org/pep-0701/
2024-02-16 20:28:11 +05:30
Jacob Coffee
c47ff658e4 chore(docs): update Discord invite to permalink (#10005)
## Summary

Update the Discord unique-id invite links to use the company permalink.

## Test Plan

Visiting the links
2024-02-15 23:16:02 -05:00
Adrien Ball
c3bba54b6b Fix SIM113 false positive with async for loops (#9996)
## Summary
Ignore `async for` loops when checking the SIM113 rule.

Closes #9995 

## Test Plan
A new test case was added to SIM113.py with an async for loop.
2024-02-15 22:40:01 -05:00
Micha Reiser
fe79798c12 split string module (#9987) 2024-02-14 18:54:55 +01:00
393 changed files with 12698 additions and 5570 deletions

6
.github/CODEOWNERS vendored
View File

@@ -7,3 +7,9 @@
# Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood

View File

@@ -133,7 +133,7 @@ jobs:
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ruff
path: target/debug/ruff
@@ -238,7 +238,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -250,6 +250,7 @@ jobs:
with:
name: ruff
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- name: Install ruff-ecosystem
@@ -324,13 +325,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Results
with:
name: ecosystem-result
@@ -484,7 +485,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download development ruff binary
id: ruff-target
with:

View File

@@ -52,9 +52,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload sdist"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-sdist
path: dist
macos-x86_64:
@@ -80,9 +80,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-macos-x86_64
path: dist
- name: "Archive binary"
run: |
@@ -90,9 +90,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-macos-x86_64
path: |
*.tar.gz
*.sha256
@@ -119,9 +119,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-aarch64-apple-darwin
path: dist
- name: "Archive binary"
run: |
@@ -129,9 +129,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-aarch64-apple-darwin
path: |
*.tar.gz
*.sha256
@@ -170,9 +170,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
@@ -181,9 +181,9 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.zip
*.sha256
@@ -218,9 +218,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -228,9 +228,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -251,8 +251,12 @@ jobs:
arch: s390x
- target: powerpc64le-unknown-linux-gnu
arch: ppc64le
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps:
- uses: actions/checkout@v4
@@ -285,9 +289,9 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -295,9 +299,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -337,9 +341,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -347,9 +351,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -394,9 +398,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -404,9 +408,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -463,10 +467,11 @@ jobs:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: wheels
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
@@ -506,10 +511,11 @@ jobs:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: binaries
pattern: binaries-*
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:

View File

@@ -1,5 +1,56 @@
# Breaking Changes
## 0.3.0
### Ruff 2024.2 style
The formatter now formats code according to the Ruff 2024.2 style guide. Read the [changelog](./CHANGELOG.md#030) for a detailed list of stabilized style changes.
### `isort`: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
Previously, Ruff used one or two blank lines (or the number configured by `isort.lines-after-imports`) after imports in typing stub files (`.pyi` files).
The [typing style guide for stubs](https://typing.readthedocs.io/en/latest/source/stubs.html#style-guide) recommends using at most 1 blank line for grouping.
As of this release, `isort` now always uses one blank line after imports in stub files, the same as the formatter.
### `build` is no longer excluded by default ([#10093](https://github.com/astral-sh/ruff/pull/10093))
Ruff maintains a list of directories and files that are excluded by default. This list now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git`
- `.git-rewrite`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `build` directory was included in this list. However, the `build` directory tends to be a not-unpopular directory
name, and excluding it by default caused confusion. Ruff now no longer excludes `build` except if it is excluded by a `.gitignore` file
or because it is listed in `extend-exclude`.
### `--format` is no longer a valid `rule` or `linter` command option
Previously, `ruff rule` and `ruff linter` accepted the `--format <FORMAT>` option as an alias for `--output-format`. Ruff no longer
supports this alias. Please use `ruff rule --output-format <FORMAT>` and `ruff linter --output-format <FORMAT>` instead.
## 0.1.9
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -1,5 +1,131 @@
# Changelog
## 0.3.0
This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.
### Preview features
- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154))
- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066))
- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094))
### Rule changes
- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055))
- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057))
- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027))
- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065))
- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845))
- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054))
- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060))
- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148))
### Formatter
This release introduces the Ruff 2024.2 style, stabilizing the following changes:
- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154))
- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222))
- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155))
- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240))
- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283))
- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210))
- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637))
- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280))
- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### CLI
- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135))
- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093))
- Deprecate `ruff <path>`, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check <path>`, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169))
- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170))
### Bug fixes
- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152))
- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091))
- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049))
- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162))
- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074))
- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118))
- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080))
### Performance
- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121))
- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116))
### Documentation
- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144))
- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136))
## 0.2.2
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"`).
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
### Preview features
- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642))
- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266))
- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880))
### Rule changes
- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793))
- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862))
- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887))
- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976))
- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952))
### Formatter
- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915))
- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957))
- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922))
### CLI
- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599))
### Bug fixes
- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866))
- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905))
- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969))
- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981))
- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953))
- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996))
- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859))
- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955))
- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907))
- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894))
- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878))
- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964))
- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956))
- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865))
- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990))
- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849))
### Performance
- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888))
- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853))
- Reduce `Result<Tok, LexicalError>` size by using `Box<str>` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885))
- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900))
- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867))
- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884))
## 0.2.1
This release includes support for range formatting (i.e., the ability to format specific lines

View File

@@ -39,7 +39,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
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)
@@ -316,7 +316,7 @@ To preview any changes to the documentation locally:
```
The documentation should then be available locally at
[http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
[http://127.0.0.1:8000/ruff/](http://127.0.0.1:8000/ruff/).
## Release Process

126
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "argfile"
@@ -217,9 +217,9 @@ checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bstr"
version = "1.9.0"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
"regex-automata 0.4.5",
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
"clap_derive",
@@ -322,9 +322,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstream",
"anstyle",
@@ -383,7 +383,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -407,9 +407,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb4ab4dcb6554eb4f590fb16f99d3b102ab76f5f56554c9a5340518b32c499b"
checksum = "4b85b056aa0541d1975ebc524149dde72803a5d7352b6aebf9eabc44f9017246"
dependencies = [
"colored",
"libc",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc07a3d3f7e0c8961d0ffdee149d39b231bafdcdc3d978dc5ad790c615f55f3f"
checksum = "02ae9de916d6315a5129bca2fc7957285f0b9f77a2f6a8734a0a146caee2b0b6"
dependencies = [
"codspeed",
"colored",
@@ -592,7 +592,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -603,7 +603,7 @@ checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77"
dependencies = [
"darling_core",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -914,35 +914,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hoot"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452"
dependencies = [
"httparse",
"log",
]
[[package]]
name = "hootbin"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2"
dependencies = [
"fastrand",
"hoot",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "humantime"
version = "2.1.0"
@@ -1077,9 +1048,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.34.0"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2"
dependencies = [
"console",
"globset",
@@ -1130,7 +1101,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -1709,7 +1680,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -1960,7 +1931,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -1979,7 +1950,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"anyhow",
"argfile",
@@ -2140,7 +2111,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2212,7 +2183,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2394,7 +2365,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"anyhow",
"clap",
@@ -2612,24 +2583,24 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132"
checksum = "4c1432112bce8b966497ac46519535189a3250a3812cd27a999678a69756f79f"
dependencies = [
"js-sys",
"serde",
@@ -2638,13 +2609,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2707,7 +2678,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2817,7 +2788,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2839,9 +2810,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c"
dependencies = [
"proc-macro2",
"quote",
@@ -2927,7 +2898,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2938,7 +2909,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"test-case-core",
]
@@ -2959,7 +2930,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3096,7 +3067,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3262,13 +3233,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.9.5"
version = "2.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d"
checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35"
dependencies = [
"base64",
"flate2",
"hootbin",
"log",
"once_cell",
"rustls",
@@ -3316,7 +3286,7 @@ checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3410,7 +3380,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"wasm-bindgen-shared",
]
@@ -3444,7 +3414,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3477,7 +3447,7 @@ checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3742,7 +3712,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]

View File

@@ -14,39 +14,39 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.79" }
anyhow = { version = "1.0.80" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
clap = { version = "4.5.1", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
colored = { version = "2.1.0" }
configparser = { version = "3.0.3" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version ="3.0.1"}
countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version ="0.10.1"}
env_logger = { version = "0.10.1" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version ="2.11.0"}
fs-err = { version = "2.11.0" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hexf-parse = { version ="0.2.1"}
hexf-parse = { version = "0.2.1" }
ignore = { version = "0.4.22" }
imara-diff ={ version = "0.1.5"}
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indicatif ={ version = "0.17.8"}
indoc ={ version = "2.0.4"}
insta = { version = "1.34.0", feature = ["filters", "glob"] }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
@@ -57,7 +57,7 @@ lexical-parse-float = { version = "0.8.0", features = ["format"] }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
memchr = { version = "2.7.1" }
mimalloc = { version ="0.1.39"}
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" }
@@ -75,35 +75,35 @@ regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.21" }
serde = { version = "1.0.196", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
seahash = { version = "4.1.0" }
semver = { version = "1.0.22" }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" }
shlex = { version ="1.3.0"}
shlex = { version = "1.3.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.9.0"}
syn = { version = "2.0.51" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.57" }
tikv-jemallocator = { version ="0.5.0"}
tikv-jemallocator = { version = "0.5.0" }
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version ="0.9"}
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.1" }
ureq = { version = "2.9.1" }
ureq = { version = "2.9.6" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" }

View File

@@ -8,7 +8,7 @@
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
[**Discord**](https://discord.com/invite/astral-sh) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter and code formatter, written in Rust.
@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.2.1
rev: v0.3.0
hooks:
# Run the linter.
- id: ruff
@@ -172,7 +172,7 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
```
@@ -341,14 +341,14 @@ For a complete enumeration of the supported rules, see [_Rules_](https://docs.as
Contributions are welcome and highly appreciated. To get started, check out the
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
## Support
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh).
## Acknowledgements
@@ -378,6 +378,7 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow)

View File

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

View File

@@ -63,10 +63,6 @@ pub enum Command {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
@@ -75,10 +71,6 @@ pub enum Command {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
@@ -745,38 +737,34 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
}
}
/// Enumeration of various ways in which a --config CLI flag
/// could be invalid
#[derive(Debug)]
enum TomlParseFailureKind {
SyntaxError,
UnknownOption,
enum InvalidConfigFlagReason {
InvalidToml(toml::de::Error),
/// It was valid TOML, but not a valid ruff config file.
/// E.g. the user tried to select a rule that doesn't exist,
/// or tried to enable a setting that doesn't exist
ValidTomlButInvalidRuffSchema(toml::de::Error),
/// It was a valid ruff config file, but the user tried to pass a
/// value for `extend` as part of the config override.
// `extend` is special, because it affects which config files we look at
/// in the first place. We currently only parse --config overrides *after*
/// we've combined them with all the arguments from the various config files
/// that we found, so trying to override `extend` as part of a --config
/// override is forbidden.
ExtendPassedViaConfigFlag,
}
impl std::fmt::Display for TomlParseFailureKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display = match self {
Self::SyntaxError => "The supplied argument is not valid TOML",
Self::UnknownOption => {
impl InvalidConfigFlagReason {
const fn description(&self) -> &'static str {
match self {
Self::InvalidToml(_) => "The supplied argument is not valid TOML",
Self::ValidTomlButInvalidRuffSchema(_) => {
"Could not parse the supplied argument as a `ruff.toml` configuration option"
}
};
write!(f, "{display}")
}
}
#[derive(Debug)]
struct TomlParseFailure {
kind: TomlParseFailureKind,
underlying_error: toml::de::Error,
}
impl std::fmt::Display for TomlParseFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let TomlParseFailure {
kind,
underlying_error,
} = self;
let display = format!("{kind}:\n\n{underlying_error}");
write!(f, "{}", display.trim_end())
Self::ExtendPassedViaConfigFlag => "Cannot include `extend` in a --config flag value",
}
}
}
@@ -827,18 +815,19 @@ impl TypedValueParser for ConfigArgumentParser {
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
let toml_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into() {
Ok(option) => return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option))),
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::UnknownOption,
underlying_error,
},
},
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::SyntaxError,
underlying_error,
let config_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into::<Options>() {
Ok(option) => {
if option.extend.is_none() {
return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option)));
}
InvalidConfigFlagReason::ExtendPassedViaConfigFlag
}
Err(underlying_error) => {
InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error)
}
},
Err(underlying_error) => InvalidConfigFlagReason::InvalidToml(underlying_error),
};
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
@@ -853,6 +842,21 @@ impl TypedValueParser for ConfigArgumentParser {
clap::error::ContextValue::String(value.to_string()),
);
let underlying_error = match &config_parse_error {
InvalidConfigFlagReason::ExtendPassedViaConfigFlag => {
let tip = config_parse_error.description().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
return Err(new_error);
}
InvalidConfigFlagReason::InvalidToml(underlying_error)
| InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error) => {
underlying_error
}
};
// small hack so that multiline tips
// have the same indent on the left-hand side:
let tip_indent = " ".repeat(" tip: ".len());
@@ -881,12 +885,16 @@ The path `{value}` does not exist"
));
}
} else if value.contains('=') {
tip.push_str(&format!("\n\n{toml_parse_error}"));
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
}
let tip = tip.trim_end().to_owned().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip.into()]),
clap::error::ContextValue::StyledStrs(vec![tip]),
);
Err(new_error)

View File

@@ -18,7 +18,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
use crate::args::{Args, CheckCommand, Command, FormatCommand, HelpFormat};
use crate::args::{Args, CheckCommand, Command, FormatCommand};
use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args;
@@ -114,20 +114,12 @@ fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
}
}
/// Get the actual value of the `format` desired from either `output_format`
/// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if format.is_some() {
warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
}
format.unwrap_or(output_format)
}
pub fn run(
Args {
command,
log_level_args,
}: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> {
{
let default_panic_hook = std::panic::take_hook();
@@ -158,6 +150,10 @@ pub fn run(
let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command {
Command::Version { output_format } => {
commands::version::version(output_format)?;
@@ -166,10 +162,8 @@ pub fn run(
Command::Rule {
rule,
all,
format,
mut output_format,
output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
if all {
commands::rule::rules(output_format)?;
}
@@ -182,11 +176,7 @@ pub fn run(
commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success)
}
Command::Linter {
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
Command::Linter { output_format } => {
commands::linter::linter(output_format)?;
Ok(ExitStatus::Success)
}

View File

@@ -27,26 +27,42 @@ pub fn main() -> ExitCode {
let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
if let Some(arg) = args.get(1) {
if arg
.to_str()
.is_some_and(|arg| !Command::has_subcommand(rewrite_legacy_subcommand(arg)))
// We can't use `warn_user` here because logging isn't set up at this point
// and we also don't know if the user runs ruff with quiet.
// Keep the message and pass it to `run` that is responsible for emitting the warning.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) {
// Deprecated aliases that are handled by clap
Some("--explain") => {
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.")
}
Some("--clean") => {
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
Some(arg) if !Command::has_subcommand(arg)
&& arg != "-h"
&& arg != "--help"
&& arg != "-V"
&& arg != "--version"
&& arg != "help"
{
args.insert(1, "check".into());
}
}
&& arg != "help" => {
{
args.insert(1, "check".into());
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.")
}
},
_ => None
};
let args = Args::parse_from(args);
match run(args) {
match run(args, deprecated_alias_warning) {
Ok(code) => code.into(),
Err(err) => {
#[allow(clippy::print_stderr)]
@@ -65,12 +81,3 @@ pub fn main() -> ExitCode {
}
}
}
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
match cmd {
"--explain" => "rule",
"--clean" => "clean",
"--generate-shell-completion" => "generate-shell-completion",
cmd => cmd,
}
}

View File

@@ -12,9 +12,10 @@ const STDIN: &str = "l = 1";
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
cmd.arg("--output-format");
cmd.arg(output_format);
cmd.arg("--no-cache");
cmd.arg("check")
.arg("--output-format")
.arg(output_format)
.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");

View File

@@ -7,10 +7,15 @@ use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use regex::escape;
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
}
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
@@ -147,28 +152,29 @@ fn too_many_config_files() -> Result<()> {
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config={}` or `--config={}`.
For more information, try `--help`.
",
ruff_dot_toml.display(),
ruff2_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
Ok(())
}
@@ -177,27 +183,29 @@ fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: The argument `--config={}` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
",
ruff_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--isolated")
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
Ok(())
}
@@ -350,58 +358,52 @@ def f(x):
'''
pass
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
.. code-block:: python
.. code-block:: python
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
)
Another example:
```py
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
)
```
Another example:
And another:
```py
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
>>> foo, bar, quux = (
... this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
... )
"""
pass
And another:
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
----- stderr -----
"###);
Ok(())
}

View File

@@ -71,6 +71,7 @@ impl<'a> RuffCheck<'a> {
/// Generate a [`Command`] for the `ruff check` command.
fn build(self) -> Command {
let mut cmd = ruff_cmd();
cmd.arg("check");
if let Some(output_format) = self.output_format {
cmd.args(["--output-format", output_format]);
}
@@ -805,13 +806,13 @@ fn full_output_format() {
}
#[test]
fn explain_status_codes_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "F401"]));
fn rule_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401"]));
}
#[test]
fn explain_status_codes_ruf404() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "RUF404"]), @r###"
fn rule_invalid_rule_name() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404"]), @r###"
success: false
exit_code: 2
----- stdout -----
@@ -1348,7 +1349,7 @@ fn unreadable_pyproject_toml() -> Result<()> {
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args).err().context("Unexpected success")?;
let err = run(args, None).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)

View File

@@ -12,7 +12,7 @@ use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "concise"];
const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
@@ -246,7 +246,6 @@ OTHER = "OTHER"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
@@ -293,7 +292,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -386,7 +384,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -435,7 +432,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -495,7 +491,6 @@ ignore = ["D203", "D212"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS)
, @r###"
success: true
@@ -595,6 +590,24 @@ fn too_many_config_files() -> Result<()> {
Ok(())
}
#[test]
fn extend_passed_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "extend = 'foo.toml'", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend = 'foo.toml'' for '--config <CONFIG_OPTION>'
tip: Cannot include `extend` in a --config flag value
For more information, try '--help'.
"###);
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -903,7 +916,6 @@ include = ["*.ipy"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--extension", "ipy:ipynb"])

View File

@@ -3,7 +3,7 @@ source: crates/ruff/tests/integration_test.rs
info:
program: ruff
args:
- "--explain"
- rule
- F401
---
success: true
@@ -68,4 +68,3 @@ else:
- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
----- stderr -----

View File

@@ -44,7 +44,6 @@ file_resolver.exclude = [
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
@@ -232,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}(-\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0
@@ -366,4 +365,3 @@ formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
----- stderr -----

View File

@@ -78,27 +78,28 @@ impl<'a> PrintQueue<'a> {
impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?;
elements.next().or_else(|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
})
elements.next().or_else(
#[cold]
|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?;
match slice.as_slice().first() {
Some(element) => Some(element),
None => {
if let Some(next_elements) = slices.next() {
next_elements.as_slice().first()
} else {
None
}
}
}
slice.as_slice().first().or_else(
#[cold]
|| {
slices
.next()
.and_then(|next_elements| next_elements.as_slice().first())
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
@@ -146,24 +147,30 @@ impl<'a, 'print> FitsQueue<'a, 'print> {
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else(|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
})
self.queue.pop().or_else(
#[cold]
|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else(|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
})
self.queue.top_with_interned().or_else(
#[cold]
|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {

View File

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

View File

@@ -28,3 +28,11 @@ dictionary = {
}
#import os # noqa
# case 1:
# try:
# try: # with comment
# try: print()
# except:
# except Foo:
# except Exception as e: print(e)

View File

@@ -119,3 +119,16 @@ def func(x: bool):
settings(True)
from dataclasses import dataclass, InitVar
@dataclass
class Fit:
force: InitVar[bool] = False
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)

View File

@@ -193,3 +193,11 @@ def func():
for y in range(5):
g(x, idx)
idx += 1
async def func():
# OK (for loop is async)
idx = 0
async for x in async_gen():
g(x, idx)
idx += 1

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
class Thing(object):
name: str
def __init__(self, name: str):
self.name = name

View File

@@ -147,3 +147,9 @@ ham[upper : ]
#: E203:1:10
ham[upper :]
#: Okay
ham[lower +1 :, "columnname"]
#: E203:1:13
ham[lower + 1 :, "columnname"]

View File

@@ -0,0 +1,7 @@
import os
os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")
del os.environ["WORLD_SIZE"]
import torch

View File

@@ -14,3 +14,6 @@ class Chassis(RobotModuleTemplate):
" \
\
'''blank line with whitespace
inside a multiline string'''

View File

@@ -57,3 +57,15 @@ def func():
Returns:
the value"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""

View File

@@ -0,0 +1,32 @@
from typing import Any
d = {1: 1, 2: 2}
d_tuple = {(1, 2): 3, (4, 5): 6}
d_tuple_annotated: Any = {(1, 2): 3, (4, 5): 6}
d_tuple_incorrect_tuple = {(1,): 3, (4, 5): 6}
l = [1, 2]
s1 = {1, 2}
s2 = {1, 2, 3}
# Errors
for k, v in d:
pass
for k, v in d_tuple_incorrect_tuple:
pass
# Non errors
for k, v in d.items():
pass
for k in d.keys():
pass
for i, v in enumerate(l):
pass
for i, v in s1.intersection(s2):
pass
for a, b in d_tuple:
pass
for a, b in d_tuple_annotated:
pass

View File

@@ -17,3 +17,14 @@ class Fruit:
return choice(Fruit.COLORS)
pick_one_color = staticmethod(pick_one_color)
class Class:
def class_method(cls):
pass
class_method = classmethod(class_method);another_statement
def static_method():
pass
static_method = staticmethod(static_method);

View File

@@ -51,3 +51,7 @@ foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.
import sys
sys.platform == "win32" or sys.platform == "emscripten" # sys attributes

View File

@@ -33,7 +33,7 @@ bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
# These become literals
str()
str("foo")
str("""
@@ -53,3 +53,9 @@ bool(False)
# These become a literal but retain parentheses
int(1).denominator
# These too are literals in spirit
int(+1)
int(-1)
float(+1.0)
float(-1.0)

View File

@@ -57,6 +57,16 @@ revision_heads_map_ast = [
list(zip(x, y))[0]
[*zip(x, y)][0]
# RUF015 (pop)
list(x).pop(0)
[i for i in x].pop(0)
list(i for i in x).pop(0)
# OK
list(x).pop(1)
list(x).remove(0)
list(x).remove(1)
def test():
zip = list # Overwrite the builtin zip

View File

@@ -68,3 +68,7 @@ def method_calls():
first = "Wendy"
last = "Appleseed"
value.method("{first} {last}") # RUF027
def format_specifiers():
a = 4
b = "{a:b} {a:^5}"

View File

@@ -25,6 +25,10 @@ def negative_cases():
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a=5)
formatted = "{a}".fmt(a=7)
partial = "partial sentence"
a = _("formatting of {partial} in a translation string is bad practice")
_("formatting of {partial} in a translation string is bad practice")
print(_("formatting of {partial} in a translation string is bad practice"))
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a=5)))
print(format(do_nothing("{a}"), a=5))

View File

@@ -0,0 +1,2 @@
# 测试eval函数,eval()函数用来执行一个字符串表达式并返t表达式的值。另外可以讲字符串转换成列表或元组或字典
a = "{1: 'a', 2: 'b'}"

View File

@@ -0,0 +1,63 @@
def fmt_off_between_lists():
test_list = [
# fmt: off
1,
2,
3,
]
# note: the second `fmt: skip`` should be OK
def fmt_skip_on_own_line():
# fmt: skip
pass # fmt: skip
@fmt_skip_on_own_line
# fmt: off
@fmt_off_between_lists
def fmt_off_between_decorators():
pass
@fmt_off_between_decorators
# fmt: off
class FmtOffBetweenClassDecorators:
...
def fmt_off_in_else():
x = [1, 2, 3]
for val in x:
print(x)
# fmt: off
else:
print("done")
while False:
print("while")
# fmt: off
# fmt: off
else:
print("done")
if len(x) > 3:
print("huh?")
# fmt: on
# fmt: off
else:
print("expected")
class Test:
@classmethod
# fmt: off
def cls_method_a(
# fmt: off
cls,
) -> None: # noqa: test # fmt: skip
pass
def fmt_on_trailing():
# fmt: off
val = 5 # fmt: on
pass # fmt: on

View File

@@ -116,7 +116,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_simplify::rules::use_capital_environment_variables(checker, expr);
}
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, subscript);
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
}
if checker.enabled(Rule::InvalidIndexType) {
ruff::rules::invalid_index_type(checker, subscript);
@@ -134,6 +134,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
elts,
ctx,
range: _,
parenthesized: _,
})
| Expr::List(ast::ExprList {
elts,
@@ -964,6 +965,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DefaultFactoryKwarg) {
ruff::rules::default_factory_kwarg(checker, call);
}
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
@@ -1451,6 +1455,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
generators,
elt: _,
range: _,
parenthesized: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {

View File

@@ -2,11 +2,14 @@ use ruff_python_ast::Suite;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::flake8_bugbear;
use crate::rules::{flake8_bugbear, ruff};
/// Run lint rules over a module.
pub(crate) fn module(suite: &Suite, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, suite);
}
if checker.enabled(Rule::InvalidFormatterSuppressionComment) {
ruff::rules::ignored_formatter_suppression_comment(checker, suite);
}
}

View File

@@ -386,10 +386,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
},
) => {
if checker.enabled(Rule::NoClassmethodDecorator) {
pylint::rules::no_classmethod_decorator(checker, class_def);
pylint::rules::no_classmethod_decorator(checker, stmt);
}
if checker.enabled(Rule::NoStaticmethodDecorator) {
pylint::rules::no_staticmethod_decorator(checker, class_def);
pylint::rules::no_staticmethod_decorator(checker, stmt);
}
if checker.enabled(Rule::DjangoNullableModelStringField) {
flake8_django::rules::nullable_model_string_field(checker, body);
@@ -1299,6 +1299,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
}
if checker.enabled(Rule::DictIterMissingItems) {
pylint::rules::dict_iter_missing_items(checker, target, iter);
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, target, body);
}

View File

@@ -2,10 +2,16 @@ use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi};
use crate::rules::{flake8_bandit, flake8_pyi, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
if checker.any_enabled(&[
Rule::AmbiguousUnicodeCharacterString,
Rule::AmbiguousUnicodeCharacterDocstring,
]) {
ruff::rules::ambiguous_unicode_character_string(checker, string_like);
}
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
}

View File

@@ -1,3 +1,4 @@
use ruff_python_ast::StmtFunctionDef;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use crate::rules::flake8_type_checking;
@@ -26,6 +27,8 @@ pub(super) enum AnnotationContext {
}
impl AnnotationContext {
/// Determine the [`AnnotationContext`] for an annotation based on the current scope of the
/// semantic model.
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
// If the annotation is in a class scope (e.g., an annotated assignment for a
// class field) or a function scope, and that class or function is marked as
@@ -71,4 +74,23 @@ impl AnnotationContext {
Self::TypingOnly
}
/// Determine the [`AnnotationContext`] to use for annotations in a function signature.
pub(super) fn from_function(
function_def: &StmtFunctionDef,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> Self {
if flake8_type_checking::helpers::runtime_required_function(
function_def,
&settings.flake8_type_checking.runtime_required_decorators,
semantic,
) {
Self::RuntimeRequired
} else if semantic.future_annotations() {
Self::TypingOnly
} else {
Self::RuntimeEvaluated
}
}
}

View File

@@ -374,7 +374,9 @@ where
|| helpers::is_assignment_to_a_dunder(stmt)
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| imports::is_sys_path_modification(stmt, self.semantic()))
|| imports::is_sys_path_modification(stmt, self.semantic())
|| (self.settings.preview.is_enabled()
&& imports::is_os_environ_modification(stmt, self.semantic())))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -582,7 +584,8 @@ where
// Function annotations are always evaluated at runtime, unless future annotations
// are enabled.
let runtime_annotation = !self.semantic.future_annotations();
let annotation =
AnnotationContext::from_function(function_def, &self.semantic, self.settings);
// The first parameter may be a single dispatch.
let mut singledispatch =
@@ -606,10 +609,18 @@ where
if let Some(expr) = &parameter_with_default.parameter.annotation {
if singledispatch {
self.visit_runtime_required_annotation(expr);
} else if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
};
}
if let Some(expr) = &parameter_with_default.default {
@@ -619,28 +630,46 @@ where
}
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
}
if let Some(arg) = &parameters.kwarg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
}
for expr in returns {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
let definition = docstrings::extraction::extract_definition(
@@ -978,6 +1007,7 @@ where
elt,
generators,
range: _,
parenthesized: _,
}) => {
self.visit_generators(generators);
self.visit_expr(elt);
@@ -1298,6 +1328,7 @@ where
elts,
ctx,
range: _,
parenthesized: _,
}) = slice.as_ref()
{
let mut iter = elts.iter();

View File

@@ -3,12 +3,13 @@
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange};
use ruff_text_size::Ranged;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
use crate::fix::edits::delete_comment;
use crate::noqa;
use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping};
use crate::registry::{AsRule, Rule, RuleSet};
@@ -111,7 +112,8 @@ pub(crate) fn check_noqa(
if line.matches.is_empty() {
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
diagnostic.set_fix(Fix::safe_edit(delete_noqa(directive.range(), locator)));
diagnostic
.set_fix(Fix::safe_edit(delete_comment(directive.range(), locator)));
diagnostics.push(diagnostic);
}
@@ -177,8 +179,10 @@ pub(crate) fn check_noqa(
directive.range(),
);
if valid_codes.is_empty() {
diagnostic
.set_fix(Fix::safe_edit(delete_noqa(directive.range(), locator)));
diagnostic.set_fix(Fix::safe_edit(delete_comment(
directive.range(),
locator,
)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
@@ -195,48 +199,3 @@ pub(crate) fn check_noqa(
ignored_diagnostics.sort_unstable();
ignored_diagnostics
}
/// Generate a [`Edit`] to delete a `noqa` directive.
fn delete_noqa(range: TextRange, locator: &Locator) -> Edit {
let line_range = locator.line_range(range.start());
// Compute the leading space.
let prefix = locator.slice(TextRange::new(line_range.start(), range.start()));
let leading_space_len = prefix.text_len() - prefix.trim_whitespace_end().text_len();
// Compute the trailing space.
let suffix = locator.slice(TextRange::new(range.end(), line_range.end()));
let trailing_space_len = suffix.text_len() - suffix.trim_whitespace_start().text_len();
// Ex) `# noqa`
if line_range
== TextRange::new(
range.start() - leading_space_len,
range.end() + trailing_space_len,
)
{
let full_line_end = locator.full_line_end(line_range.end());
Edit::deletion(line_range.start(), full_line_end)
}
// Ex) `x = 1 # noqa`
else if range.end() + trailing_space_len == line_range.end() {
Edit::deletion(range.start() - leading_space_len, line_range.end())
}
// Ex) `x = 1 # noqa # type: ignore`
else if locator
.slice(TextRange::new(
range.end() + trailing_space_len,
line_range.end(),
))
.starts_with('#')
{
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,
)
}
}

View File

@@ -6,17 +6,14 @@ use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_diagnostics::Diagnostic;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::directives::TodoComment;
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::BlankLinesChecker;
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
@@ -66,31 +63,15 @@ pub(crate) fn check_tokens(
pylint::rules::empty_comments(&mut diagnostics, indexer, locator);
}
if settings.rules.any_enabled(&[
Rule::AmbiguousUnicodeCharacterString,
Rule::AmbiguousUnicodeCharacterDocstring,
Rule::AmbiguousUnicodeCharacterComment,
]) {
let mut state_machine = StateMachine::default();
for &(ref tok, range) in tokens.iter().flatten() {
let is_docstring = state_machine.consume(tok);
let context = match tok {
Tok::String { .. } => {
if is_docstring {
Context::Docstring
} else {
Context::String
}
}
Tok::FStringMiddle { .. } => Context::String,
Tok::Comment(_) => Context::Comment,
_ => continue,
};
ruff::rules::ambiguous_unicode_character(
if settings
.rules
.enabled(Rule::AmbiguousUnicodeCharacterComment)
{
for range in indexer.comment_ranges() {
ruff::rules::ambiguous_unicode_character_comment(
&mut diagnostics,
locator,
range,
context,
*range,
settings,
);
}

View File

@@ -244,6 +244,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1141") => (RuleGroup::Preview, rules::pylint::rules::DictIterMissingItems),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
(Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs),
(Pylint, "E1206") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooFewArgs),
@@ -654,7 +655,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "407") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlExpatImport),
(Flake8Bandit, "408") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlMinidomImport),
(Flake8Bandit, "409") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlPulldomImport),
(Flake8Bandit, "410") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousLxmlImport),
(Flake8Bandit, "410") => (RuleGroup::Removed, rules::flake8_bandit::rules::SuspiciousLxmlImport),
(Flake8Bandit, "411") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlrpcImport),
(Flake8Bandit, "412") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousHttpoxyImport),
(Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport),
@@ -944,6 +945,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(feature = "test-rules")]

View File

@@ -62,6 +62,51 @@ pub(crate) fn delete_stmt(
}
}
/// Generate a [`Edit`] to delete a comment (for example: a `noqa` directive).
pub(crate) fn delete_comment(range: TextRange, locator: &Locator) -> Edit {
let line_range = locator.line_range(range.start());
// Compute the leading space.
let prefix = locator.slice(TextRange::new(line_range.start(), range.start()));
let leading_space_len = prefix.text_len() - prefix.trim_whitespace_end().text_len();
// Compute the trailing space.
let suffix = locator.slice(TextRange::new(range.end(), line_range.end()));
let trailing_space_len = suffix.text_len() - suffix.trim_whitespace_start().text_len();
// Ex) `# noqa`
if line_range
== TextRange::new(
range.start() - leading_space_len,
range.end() + trailing_space_len,
)
{
let full_line_end = locator.full_line_end(line_range.end());
Edit::deletion(line_range.start(), full_line_end)
}
// Ex) `x = 1 # noqa`
else if range.end() + trailing_space_len == line_range.end() {
Edit::deletion(range.start() - leading_space_len, line_range.end())
}
// Ex) `x = 1 # noqa # type: ignore`
else if locator
.slice(TextRange::new(
range.end() + trailing_space_len,
line_range.end(),
))
.starts_with('#')
{
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,
)
}
}
/// Generate a `Fix` to remove the specified imports from an `import` statement.
pub(crate) fn remove_unused_imports<'a>(
member_names: impl Iterator<Item = &'a str>,

View File

@@ -256,8 +256,6 @@ impl Rule {
| Rule::MixedSpacesAndTabs
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
| Rule::AvoidableEscapedQuote
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString

View File

@@ -26,7 +26,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static POSITIVE_CASES: Lazy<RegexSet> = Lazy::new(|| {
RegexSet::new([
// Keywords
r"^(?:elif\s+.*\s*:|else\s*:|try\s*:|finally\s*:|except\s+.*\s*:)$",
r"^(?:elif\s+.*\s*:.*|else\s*:.*|try\s*:.*|finally\s*:.*|except.*:.*|case\s+.*\s*:.*)$",
// Partial dictionary
r#"^['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#,
// Multiline assignment
@@ -147,6 +147,27 @@ mod tests {
assert!(!comment_contains_code("#to print", &[]));
}
#[test]
fn comment_contains_code_single_line() {
assert!(comment_contains_code("# case 1: print()", &[]));
assert!(comment_contains_code("# try: get(1, 2, 3)", &[]));
assert!(comment_contains_code("# else: print()", &[]));
assert!(comment_contains_code("# elif x == 10: print()", &[]));
assert!(comment_contains_code(
"# except Exception as e: print(e)",
&[]
));
assert!(comment_contains_code("# except: print()", &[]));
assert!(comment_contains_code("# finally: close_handle()", &[]));
assert!(!comment_contains_code("# try: use cache", &[]));
assert!(!comment_contains_code("# else: we should return", &[]));
assert!(!comment_contains_code(
"# call function except: without cache",
&[]
));
}
#[test]
fn comment_contains_code_with_multiline() {
assert!(comment_contains_code("#else:", &[]));
@@ -155,11 +176,15 @@ mod tests {
assert!(comment_contains_code("#elif True:", &[]));
assert!(comment_contains_code("#x = foo(", &[]));
assert!(comment_contains_code("#except Exception:", &[]));
assert!(comment_contains_code("# case 1:", &[]));
assert!(comment_contains_code("#case 1:", &[]));
assert!(comment_contains_code("# try:", &[]));
assert!(!comment_contains_code("# this is = to that :(", &[]));
assert!(!comment_contains_code("#else", &[]));
assert!(!comment_contains_code("#or else:", &[]));
assert!(!comment_contains_code("#else True:", &[]));
assert!(!comment_contains_code("# in that case:", &[]));
// Unpacking assignments
assert!(comment_contains_code(

View File

@@ -47,7 +47,8 @@ fn is_standalone_comment(line: &str) -> bool {
for char in line.chars() {
if char == '#' {
return true;
} else if !char.is_whitespace() {
}
if !char.is_whitespace() {
return false;
}
}

View File

@@ -148,4 +148,132 @@ ERA001.py:27:5: ERA001 Found commented-out code
29 28 |
30 29 | #import os # noqa
ERA001.py:32:1: ERA001 Found commented-out code
|
30 | #import os # noqa
31 |
32 | # case 1:
| ^^^^^^^^^ ERA001
33 | # try:
34 | # try: # with comment
|
= help: Remove commented-out code
Display-only fix
29 29 |
30 30 | #import os # noqa
31 31 |
32 |-# case 1:
33 32 | # try:
34 33 | # try: # with comment
35 34 | # try: print()
ERA001.py:33:1: ERA001 Found commented-out code
|
32 | # case 1:
33 | # try:
| ^^^^^^ ERA001
34 | # try: # with comment
35 | # try: print()
|
= help: Remove commented-out code
Display-only fix
30 30 | #import os # noqa
31 31 |
32 32 | # case 1:
33 |-# try:
34 33 | # try: # with comment
35 34 | # try: print()
36 35 | # except:
ERA001.py:34:1: ERA001 Found commented-out code
|
32 | # case 1:
33 | # try:
34 | # try: # with comment
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
35 | # try: print()
36 | # except:
|
= help: Remove commented-out code
Display-only fix
31 31 |
32 32 | # case 1:
33 33 | # try:
34 |-# try: # with comment
35 34 | # try: print()
36 35 | # except:
37 36 | # except Foo:
ERA001.py:35:1: ERA001 Found commented-out code
|
33 | # try:
34 | # try: # with comment
35 | # try: print()
| ^^^^^^^^^^^^^^ ERA001
36 | # except:
37 | # except Foo:
|
= help: Remove commented-out code
Display-only fix
32 32 | # case 1:
33 33 | # try:
34 34 | # try: # with comment
35 |-# try: print()
36 35 | # except:
37 36 | # except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:36:1: ERA001 Found commented-out code
|
34 | # try: # with comment
35 | # try: print()
36 | # except:
| ^^^^^^^^^ ERA001
37 | # except Foo:
38 | # except Exception as e: print(e)
|
= help: Remove commented-out code
Display-only fix
33 33 | # try:
34 34 | # try: # with comment
35 35 | # try: print()
36 |-# except:
37 36 | # except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:37:1: ERA001 Found commented-out code
|
35 | # try: print()
36 | # except:
37 | # except Foo:
| ^^^^^^^^^^^^^ ERA001
38 | # except Exception as e: print(e)
|
= help: Remove commented-out code
Display-only fix
34 34 | # try: # with comment
35 35 | # try: print()
36 36 | # except:
37 |-# except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:38:1: ERA001 Found commented-out code
|
36 | # except:
37 | # except Foo:
38 | # except Exception as e: print(e)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERA001
|
= help: Remove commented-out code
Display-only fix
35 35 | # try: print()
36 36 | # except:
37 37 | # except Foo:
38 |-# except Exception as e: print(e)

View File

@@ -211,8 +211,14 @@ impl Violation for SuspiciousXmlPulldomImport {
}
}
/// ## Removed
/// This rule was removed as the `lxml` library has been modified to address
/// known vulnerabilities and unsafe defaults. As such, the `defusedxml`
/// library is no longer necessary, `defusedxml` has [deprecated] its `lxml`
/// module.
///
/// ## What it does
/// Checks for imports of the`lxml` module.
/// Checks for imports of the `lxml` module.
///
/// ## Why is this bad?
/// Using various methods from the `lxml` module to parse untrusted XML data is
@@ -223,6 +229,8 @@ impl Violation for SuspiciousXmlPulldomImport {
/// ```python
/// import lxml
/// ```
///
/// [deprecated]: https://github.com/tiran/defusedxml/blob/c7445887f5e1bcea470a16f61369d29870cfcfe1/README.md#defusedxmllxml
#[violation]
pub struct SuspiciousLxmlImport;

View File

@@ -45,7 +45,7 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
/// 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__")
matches!(name, "__setitem__" | "__post_init__")
}
/// Returns `true` if an argument is allowed to use a boolean trap. To return

View File

@@ -109,6 +109,7 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
elts: elts.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
}
.into()
}

View File

@@ -2,7 +2,7 @@ use ast::call_path::{from_qualified_name, CallPath};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault};
use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault, Stmt};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_index::Indexer;
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
@@ -152,6 +152,11 @@ fn move_initialization(
// Set the default argument value to `None`.
let default_edit = Edit::range_replacement("None".to_string(), default.range());
// If the function is a stub, this is the only necessary edit.
if is_stub(function_def) {
return Some(Fix::unsafe_edit(default_edit));
}
// Add an `if`, to set the argument to its original value if still `None`.
let mut content = String::new();
content.push_str(&format!("if {} is None:", parameter.name.as_str()));
@@ -204,3 +209,20 @@ fn move_initialization(
let initialization_edit = Edit::insertion(content, pos);
Some(Fix::unsafe_edits(default_edit, [initialization_edit]))
}
/// Returns `true` if a function has an empty body, and is therefore a stub.
///
/// A function body is considered to be empty if it contains only `pass` statements, `...` literals,
/// and docstrings.
fn is_stub(function_def: &ast::StmtFunctionDef) -> bool {
function_def.body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
}
_ => false,
})
}

View File

@@ -19,8 +19,3 @@ B006_1.py:3:22: B006 [*] Do not use mutable data structures for argument default
3 |+def foobar(foor, bar=None):
4 4 | """
5 5 | """
6 |+
7 |+ if bar is None:
8 |+ bar = {}

View File

@@ -19,9 +19,4 @@ B006_2.py:4:22: B006 [*] Do not use mutable data structures for argument default
4 |-def foobar(foor, bar={}):
4 |+def foobar(foor, bar=None):
5 5 | """
6 |- """
6 |+ """
7 |+ if bar is None:
8 |+ bar = {}
6 6 | """

View File

@@ -16,10 +16,5 @@ B006_3.py:4:22: B006 [*] Do not use mutable data structures for argument default
3 3 |
4 |-def foobar(foor, bar={}):
4 |+def foobar(foor, bar=None):
5 |+ """
5 6 | """
6 |- """
7 |+ if bar is None:
8 |+ bar = {}
5 5 | """
6 6 | """

View File

@@ -15,11 +15,9 @@ B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument def
62 62 |
63 |-def this_is_wrong(value=[1, 2, 3]):
63 |+def this_is_wrong(value=None):
64 |+ if value is None:
65 |+ value = [1, 2, 3]
64 66 | ...
65 67 |
66 68 |
64 64 | ...
65 65 |
66 66 |
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -35,11 +33,9 @@ B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument def
66 66 |
67 |-def this_is_also_wrong(value={}):
67 |+def this_is_also_wrong(value=None):
68 |+ if value is None:
69 |+ value = {}
68 70 | ...
69 71 |
70 72 |
68 68 | ...
69 69 |
70 70 |
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -57,11 +53,9 @@ B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument def
72 72 | @staticmethod
73 |- def this_is_also_wrong_and_more_indented(value={}):
73 |+ def this_is_also_wrong_and_more_indented(value=None):
74 |+ if value is None:
75 |+ value = {}
74 76 | pass
75 77 |
76 78 |
74 74 | pass
75 75 |
76 76 |
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -82,11 +76,9 @@ B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument def
78 |-
79 |-}):
77 |+def multiline_arg_wrong(value=None):
78 |+ if value is None:
79 |+ value = {}
80 80 | ...
81 81 |
82 82 | def single_line_func_wrong(value = {}): ...
80 78 | ...
81 79 |
82 80 | def single_line_func_wrong(value = {}): ...
B006_B008.py:82:36: B006 Do not use mutable data structures for argument defaults
|
@@ -111,11 +103,9 @@ B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument def
84 84 |
85 |-def and_this(value=set()):
85 |+def and_this(value=None):
86 |+ if value is None:
87 |+ value = set()
86 88 | ...
87 89 |
88 90 |
86 86 | ...
87 87 |
88 88 |
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -131,11 +121,9 @@ B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument def
88 88 |
89 |-def this_too(value=collections.OrderedDict()):
89 |+def this_too(value=None):
90 |+ if value is None:
91 |+ value = collections.OrderedDict()
90 92 | ...
91 93 |
92 94 |
90 90 | ...
91 91 |
92 92 |
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -151,11 +139,9 @@ B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument def
92 92 |
93 |-async def async_this_too(value=collections.defaultdict()):
93 |+async def async_this_too(value=None):
94 |+ if value is None:
95 |+ value = collections.defaultdict()
94 96 | ...
95 97 |
96 98 |
94 94 | ...
95 95 |
96 96 |
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -166,16 +152,14 @@ B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument def
= help: Replace with `None`; initialize within function
Unsafe fix
94 94 | ...
95 95 |
96 96 |
97 |-def dont_forget_me(value=collections.deque()):
97 |+def dont_forget_me(value=None):
98 |+ if value is None:
99 |+ value = collections.deque()
98 100 | ...
99 101 |
100 102 |
94 94 | ...
95 95 |
96 96 |
97 |-def dont_forget_me(value=collections.deque()):
97 |+def dont_forget_me(value=None):
98 98 | ...
99 99 |
100 100 |
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -192,11 +176,9 @@ B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument de
101 101 | # N.B. we're also flagging the function call in the comprehension
102 |-def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
102 |+def list_comprehension_also_not_okay(default=None):
103 |+ if default is None:
104 |+ default = [i ** 2 for i in range(3)]
103 105 | pass
104 106 |
105 107 |
103 103 | pass
104 104 |
105 105 |
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -212,11 +194,9 @@ B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument de
105 105 |
106 |-def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
106 |+def dict_comprehension_also_not_okay(default=None):
107 |+ if default is None:
108 |+ default = {i: i ** 2 for i in range(3)}
107 109 | pass
108 110 |
109 111 |
107 107 | pass
108 108 |
109 109 |
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -232,11 +212,9 @@ B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument de
109 109 |
110 |-def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
110 |+def set_comprehension_also_not_okay(default=None):
111 |+ if default is None:
112 |+ default = {i ** 2 for i in range(3)}
111 113 | pass
112 114 |
113 115 |
111 111 | pass
112 112 |
113 113 |
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -252,11 +230,9 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
113 113 |
114 |-def kwonlyargs_mutable(*, value=[]):
114 |+def kwonlyargs_mutable(*, value=None):
115 |+ if value is None:
116 |+ value = []
115 117 | ...
116 118 |
117 119 |
115 115 | ...
116 116 |
117 117 |
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -274,11 +250,9 @@ B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument de
238 238 | # We should handle arbitrary nesting of these B008.
239 |-def nested_combo(a=[float(3), dt.datetime.now()]):
239 |+def nested_combo(a=None):
240 |+ if a is None:
241 |+ a = [float(3), dt.datetime.now()]
240 242 | pass
241 243 |
242 244 |
240 240 | pass
241 241 |
242 242 |
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -299,12 +273,6 @@ B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument de
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if a is None:
282 |+ a = []
281 283 | pass
282 284 |
283 285 |
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -326,11 +294,6 @@ B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument de
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if b is None:
282 |+ b = {}
281 283 | pass
282 284 |
283 285 |
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -351,11 +314,7 @@ B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument de
278 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if c is None:
282 |+ c = set()
281 283 | pass
282 284 |
283 285 |
281 281 | pass
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -375,11 +334,8 @@ B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument de
279 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 280 | ):
281 |+ if d is None:
282 |+ d = set()
281 283 | pass
282 284 |
283 285 |
281 281 | pass
282 282 |
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -396,11 +352,8 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
284 |+def single_line_func_wrong(value: dict[str, str] = None):
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 |
287 289 |
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
286 286 |
287 287 |
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -418,11 +371,8 @@ B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument de
288 |-def single_line_func_wrong(value: dict[str, str] = {}):
288 |+def single_line_func_wrong(value: dict[str, str] = None):
289 289 | """Docstring"""
290 |+ if value is None:
291 |+ value = {}
290 292 | ...
291 293 |
292 294 |
290 290 | ...
291 291 |
B006_B008.py:293:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -438,11 +388,9 @@ B006_B008.py:293:52: B006 [*] Do not use mutable data structures for argument de
292 292 |
293 |-def single_line_func_wrong(value: dict[str, str] = {}):
293 |+def single_line_func_wrong(value: dict[str, str] = None):
294 |+ if value is None:
295 |+ value = {}
294 296 | """Docstring"""; ...
295 297 |
296 298 |
294 294 | """Docstring"""; ...
295 295 |
296 296 |
B006_B008.py:297:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -459,11 +407,9 @@ B006_B008.py:297:52: B006 [*] Do not use mutable data structures for argument de
296 296 |
297 |-def single_line_func_wrong(value: dict[str, str] = {}):
297 |+def single_line_func_wrong(value: dict[str, str] = None):
298 |+ if value is None:
299 |+ value = {}
298 300 | """Docstring"""; \
299 301 | ...
300 302 |
298 298 | """Docstring"""; \
299 299 | ...
300 300 |
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
@@ -485,11 +431,8 @@ B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument de
304 |-}):
302 |+def single_line_func_wrong(value: dict[str, str] = None):
305 303 | """Docstring"""
304 |+ if value is None:
305 |+ value = {}
306 306 |
307 307 |
308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \
306 304 |
307 305 |
B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults
|
@@ -513,10 +456,5 @@ B006_B008.py:313:52: B006 [*] Do not use mutable data structures for argument de
311 311 |
312 312 |
313 |-def single_line_func_wrong(value: dict[str, str] = {}):
314 |- """Docstring without newline"""
313 |+def single_line_func_wrong(value: dict[str, str] = None):
314 |+ """Docstring without newline"""
315 |+ if value is None:
316 |+ value = {}
314 314 | """Docstring without newline"""

View File

@@ -15,8 +15,4 @@ B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument
16 16 |
17 |-def error_due_to_missing_import(foo: ImmutableTypeA = []):
17 |+def error_due_to_missing_import(foo: ImmutableTypeA = None):
18 |+ if foo is None:
19 |+ foo = []
18 20 | ...
18 18 | ...

View File

@@ -29,7 +29,7 @@ enum TokenType {
/// Simplified token specialized for the task.
#[derive(Copy, Clone)]
struct Token {
r#type: TokenType,
ty: TokenType,
range: TextRange,
}
@@ -40,13 +40,13 @@ impl Ranged for Token {
}
impl Token {
fn new(r#type: TokenType, range: TextRange) -> Self {
Self { r#type, range }
fn new(ty: TokenType, range: TextRange) -> Self {
Self { ty, range }
}
fn irrelevant() -> Token {
Token {
r#type: TokenType::Irrelevant,
ty: TokenType::Irrelevant,
range: TextRange::default(),
}
}
@@ -54,7 +54,7 @@ impl Token {
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
let r#type = match tok {
let ty = match tok {
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
@@ -75,7 +75,7 @@ impl From<(&Tok, TextRange)> for Token {
_ => TokenType::Irrelevant,
};
#[allow(clippy::inconsistent_struct_constructor)]
Self { range, r#type }
Self { range, ty }
}
}
@@ -102,16 +102,13 @@ enum ContextType {
/// Comma context - described a comma-delimited "situation".
#[derive(Copy, Clone)]
struct Context {
r#type: ContextType,
ty: ContextType,
num_commas: u32,
}
impl Context {
const fn new(r#type: ContextType) -> Self {
Self {
r#type,
num_commas: 0,
}
const fn new(ty: ContextType) -> Self {
Self { ty, num_commas: 0 }
}
fn inc(&mut self) {
@@ -277,9 +274,7 @@ pub(crate) fn trailing_commas(
let mut stack = vec![Context::new(ContextType::No)];
for token in tokens {
if prev.r#type == TokenType::NonLogicalNewline
&& token.r#type == TokenType::NonLogicalNewline
{
if prev.ty == TokenType::NonLogicalNewline && token.ty == TokenType::NonLogicalNewline {
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
continue;
@@ -288,87 +283,18 @@ pub(crate) fn trailing_commas(
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.r#type == TokenType::ClosingBracket
&& match context.r#type {
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
ContextType::Subscript => context.num_commas != 0,
ContextType::List => true,
ContextType::Dict => true,
// Lambdas are required to be a single line, trailing comma never makes sense.
ContextType::LambdaParameters => false,
};
// Is prev a prohibited trailing comma?
let comma_prohibited = prev.r#type == TokenType::Comma && {
// Is `(1,)` or `x[1,]`?
let is_singleton_tuplish =
matches!(context.r#type, ContextType::Subscript | ContextType::Tuple)
&& context.num_commas <= 1;
// There was no non-logical newline, so prohibit (except in `(1,)` or `x[1,]`).
if comma_allowed && !is_singleton_tuplish {
true
// Lambdas not handled by comma_allowed so handle it specially.
} else {
context.r#type == ContextType::LambdaParameters && token.r#type == TokenType::Colon
}
};
if comma_prohibited {
let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range())));
diagnostics.push(diagnostic);
}
// Is prev a prohibited trailing comma on a bare tuple?
// Approximation: any comma followed by a statement-ending newline.
let bare_comma_prohibited =
prev.r#type == TokenType::Comma && token.r#type == TokenType::Newline;
if bare_comma_prohibited {
diagnostics.push(Diagnostic::new(TrailingCommaOnBareTuple, prev.range()));
}
// Comma is required if:
// - It is allowed,
// - Followed by a newline,
// - Not already present,
// - Not on an empty (), {}, [].
let comma_required = comma_allowed
&& prev.r#type == TokenType::NonLogicalNewline
&& !matches!(
prev_prev.r#type,
TokenType::Comma
| TokenType::OpeningBracket
| TokenType::OpeningSquareBracket
| TokenType::OpeningCurlyBracket
);
if comma_required {
let mut diagnostic =
Diagnostic::new(MissingTrailingComma, TextRange::empty(prev_prev.end()));
// Create a replacement that includes the final bracket (or other token),
// rather than just inserting a comma at the end. This prevents the UP034 fix
// removing any brackets in the same linter pass - doing both at the same time could
// lead to a syntax error.
let contents = locator.slice(prev_prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{contents},"),
prev_prev.range(),
)));
if let Some(diagnostic) = check_token(token, prev, prev_prev, context, locator) {
diagnostics.push(diagnostic);
}
// Pop the current context if the current token ended it.
// The top context is never popped (if unbalanced closing brackets).
let pop_context = match context.r#type {
let pop_context = match context.ty {
// Lambda terminated by `:`.
ContextType::LambdaParameters => token.r#type == TokenType::Colon,
ContextType::LambdaParameters => token.ty == TokenType::Colon,
// All others terminated by a closing bracket.
// flake8-commas doesn't verify that it matches the opening...
_ => token.r#type == TokenType::ClosingBracket,
_ => token.ty == TokenType::ClosingBracket,
};
if pop_context && stack.len() > 1 {
stack.pop();
@@ -379,21 +305,107 @@ pub(crate) fn trailing_commas(
}
}
fn check_token(
token: Token,
prev: Token,
prev_prev: Token,
context: Context,
locator: &Locator,
) -> Option<Diagnostic> {
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.ty == TokenType::ClosingBracket
&& match context.ty {
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
ContextType::Subscript => context.num_commas != 0,
ContextType::List => true,
ContextType::Dict => true,
// Lambdas are required to be a single line, trailing comma never makes sense.
ContextType::LambdaParameters => false,
};
// Is prev a prohibited trailing comma?
let comma_prohibited = prev.ty == TokenType::Comma && {
// Is `(1,)` or `x[1,]`?
let is_singleton_tuplish =
matches!(context.ty, ContextType::Subscript | ContextType::Tuple)
&& context.num_commas <= 1;
// There was no non-logical newline, so prohibit (except in `(1,)` or `x[1,]`).
if comma_allowed && !is_singleton_tuplish {
true
// Lambdas not handled by comma_allowed so handle it specially.
} else {
context.ty == ContextType::LambdaParameters && token.ty == TokenType::Colon
}
};
if comma_prohibited {
let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range())));
return Some(diagnostic);
}
// Is prev a prohibited trailing comma on a bare tuple?
// Approximation: any comma followed by a statement-ending newline.
let bare_comma_prohibited = prev.ty == TokenType::Comma && token.ty == TokenType::Newline;
if bare_comma_prohibited {
return Some(Diagnostic::new(TrailingCommaOnBareTuple, prev.range()));
}
if !comma_allowed {
return None;
}
// Comma is required if:
// - It is allowed,
// - Followed by a newline,
// - Not already present,
// - Not on an empty (), {}, [].
let comma_required = prev.ty == TokenType::NonLogicalNewline
&& !matches!(
prev_prev.ty,
TokenType::Comma
| TokenType::OpeningBracket
| TokenType::OpeningSquareBracket
| TokenType::OpeningCurlyBracket
);
if comma_required {
let mut diagnostic =
Diagnostic::new(MissingTrailingComma, TextRange::empty(prev_prev.end()));
// Create a replacement that includes the final bracket (or other token),
// rather than just inserting a comma at the end. This prevents the UP034 fix
// removing any brackets in the same linter pass - doing both at the same time could
// lead to a syntax error.
let contents = locator.slice(prev_prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{contents},"),
prev_prev.range(),
)));
Some(diagnostic)
} else {
None
}
}
fn update_context(
token: Token,
prev: Token,
prev_prev: Token,
stack: &mut Vec<Context>,
) -> Context {
let new_context = match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def) => Context::new(ContextType::FunctionParameters),
(TokenType::Named | TokenType::ClosingBracket, _) => {
Context::new(ContextType::CallArguments)
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)
}

View File

@@ -29,6 +29,20 @@ import os
r"
# Copyright (C) 2023
import os
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
assert_messages!(diagnostics);
}
#[test]
fn notice_with_unicode_c() {
let diagnostics = test_snippet(
r"
# Copyright © 2023
import os
"
.trim(),

View File

@@ -15,7 +15,7 @@ pub struct Settings {
}
pub static COPYRIGHT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*").unwrap());
Lazy::new(|| Regex::new(r"(?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*").unwrap());
impl Default for Settings {
fn default() -> Self {

View File

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

View File

@@ -173,6 +173,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
let node1 = Expr::Name(ast::ExprName {
id: arg_name.into(),

View File

@@ -72,6 +72,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts,
range: _,
ctx: _,
parenthesized: _,
}) = slice.as_ref()
{
for expr in elts {
@@ -123,6 +124,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts: literal_exprs.into_iter().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
ctx: ExprContext::Load,
@@ -148,6 +150,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts,
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
ctx: ExprContext::Load,

View File

@@ -130,6 +130,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),
@@ -151,6 +152,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
elts: exprs.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),

View File

@@ -337,6 +337,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("({})", checker.generator().expr(&node)),
@@ -444,6 +445,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
elts: elts.clone(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("({})", checker.generator().expr(&node)),

View File

@@ -428,6 +428,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
let node1 = ast::ExprName {
id: "isinstance".into(),
@@ -543,6 +544,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
elts: comparators.into_iter().map(Clone::clone).collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
let node1 = ast::ExprName {
id: id.into(),
@@ -718,7 +720,7 @@ fn get_short_circuit_edit(
generator.expr(expr)
};
Edit::range_replacement(
if matches!(expr, Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _}) if !elts.is_empty())
if matches!(expr, Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _, parenthesized: _}) if !elts.is_empty())
{
format!("({content})")
} else {

View File

@@ -49,6 +49,11 @@ impl Violation for EnumerateForLoop {
/// SIM113
pub(crate) fn enumerate_for_loop(checker: &mut Checker, for_stmt: &ast::StmtFor) {
// If the loop is async, abort.
if for_stmt.is_async {
return;
}
// If the loop contains a `continue`, abort.
let mut visitor = LoopControlFlowVisitor::default();
visitor.visit_body(&for_stmt.body);

View File

@@ -382,6 +382,7 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
range: TextRange::default(),
}],
range: TextRange::default(),
parenthesized: false,
};
let node1 = ast::ExprName {
id: id.into(),

View File

@@ -30,31 +30,4 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import
13 16 |
14 17 | @attrs.define(auto_attribs=True)
runtime_evaluated_decorators_3.py:7:29: TCH003 [*] Move standard library import `collections.abc.Sequence` into a type-checking block
|
5 | from dataclasses import dataclass
6 | from uuid import UUID # TCH003
7 | from collections.abc import Sequence
| ^^^^^^^^ TCH003
8 | from pydantic import validate_call
|
= help: Move into type-checking block
Unsafe fix
4 4 | from array import array
5 5 | from dataclasses import dataclass
6 6 | from uuid import UUID # TCH003
7 |-from collections.abc import Sequence
8 7 | from pydantic import validate_call
9 8 |
10 9 | import attrs
11 10 | from attrs import frozen
11 |+from typing import TYPE_CHECKING
12 |+
13 |+if TYPE_CHECKING:
14 |+ from collections.abc import Sequence
12 15 |
13 16 |
14 17 | @attrs.define(auto_attribs=True)

View File

@@ -108,7 +108,17 @@ pub(crate) fn format_imports(
output.push_str(block_output.as_str());
}
let lines_after_imports = settings.lines_after_imports;
let lines_after_imports = if source_type.is_stub() {
// Limit the number of lines after imports in stub files to at most 1 to be compatible with the formatter.
// `isort` does the same when using the profile `isort`
match settings.lines_after_imports {
0 => 0,
_ => 1,
}
} else {
settings.lines_after_imports
};
match trailer {
None => {}
Some(Trailer::Sibling) => {
@@ -975,6 +985,7 @@ mod tests {
}
#[test_case(Path::new("lines_after_imports_nothing_after.py"))]
#[test_case(Path::new("lines_after_imports.pyi"))]
#[test_case(Path::new("lines_after_imports_func_after.py"))]
#[test_case(Path::new("lines_after_imports_class_after.py"))]
fn lines_after_imports(path: &Path) -> Result<()> {
@@ -995,6 +1006,27 @@ mod tests {
Ok(())
}
#[test_case(Path::new("lines_after_imports.pyi"))]
#[test_case(Path::new("lines_after_imports_func_after.py"))]
#[test_case(Path::new("lines_after_imports_class_after.py"))]
fn lines_after_imports_default_settings(path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy();
let mut diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&LinterSettings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
lines_after_imports: -1,
..super::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::UnsortedImports)
},
)?;
diagnostics.sort_by_key(Ranged::start);
assert_messages!(*snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("lines_between_types.py"))]
fn lines_between_types(path: &Path) -> Result<()> {
let snapshot = format!("lines_between_types{}", path.to_string_lossy());

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
lines_after_imports.pyi:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | |
3 | | from typing import Any
4 | |
5 | | from requests import Session
6 | |
7 | | from my_first_party import my_first_party_object
8 | |
9 | | from . import my_local_folder_object
10 | |
11 | |
12 | |
13 | | class Thing(object):
| |_^ I001
14 | name: str
15 | def __init__(self, name: str):
|
= help: Organize imports
Safe fix
2 2 |
3 3 | from typing import Any
4 4 |
5 |-from requests import Session
6 |-
7 5 | from my_first_party import my_first_party_object
6 |+from requests import Session
8 7 |
9 8 | from . import my_local_folder_object
10 |-
11 |-
12 9 |
13 10 | class Thing(object):
14 11 | name: str

View File

@@ -0,0 +1,38 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
lines_after_imports_class_after.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | |
3 | | from typing import Any
4 | |
5 | | from requests import Session
6 | |
7 | | from my_first_party import my_first_party_object
8 | |
9 | | from . import my_local_folder_object
10 | | class Thing(object):
| |_^ I001
11 | name: str
12 | def __init__(self, name: str):
|
= help: Organize imports
Safe fix
2 2 |
3 3 | from typing import Any
4 4 |
5 |-from requests import Session
6 |-
7 5 | from my_first_party import my_first_party_object
6 |+from requests import Session
8 7 |
9 8 | from . import my_local_folder_object
9 |+
10 |+
10 11 | class Thing(object):
11 12 | name: str
12 13 | def __init__(self, name: str):

View File

@@ -0,0 +1,55 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
lines_after_imports_func_after.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | |
3 | | from typing import Any
4 | |
5 | | from requests import Session
6 | |
7 | | from my_first_party import my_first_party_object
8 | |
9 | | from . import my_local_folder_object
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | | def main():
| |_^ I001
22 | my_local_folder_object.get()
|
= help: Organize imports
Safe fix
2 2 |
3 3 | from typing import Any
4 4 |
5 |-from requests import Session
6 |-
7 5 | from my_first_party import my_first_party_object
6 |+from requests import Session
8 7 |
9 8 | from . import my_local_folder_object
10 |-
11 |-
12 |-
13 |-
14 |-
15 |-
16 |-
17 |-
18 |-
19 9 |
20 10 |
21 11 | def main():

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
lines_after_imports.pyi:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | |
3 | | from typing import Any
4 | |
5 | | from requests import Session
6 | |
7 | | from my_first_party import my_first_party_object
8 | |
9 | | from . import my_local_folder_object
10 | |
11 | |
12 | |
13 | | class Thing(object):
| |_^ I001
14 | name: str
15 | def __init__(self, name: str):
|
= help: Organize imports
Safe fix
2 2 |
3 3 | from typing import Any
4 4 |
5 |-from requests import Session
6 |-
7 5 | from my_first_party import my_first_party_object
6 |+from requests import Session
8 7 |
9 8 | from . import my_local_folder_object
10 |-
11 |-
12 9 |
13 10 | class Thing(object):
14 11 | name: str

View File

@@ -38,6 +38,7 @@ mod tests {
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
@@ -69,6 +70,7 @@ mod tests {
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -213,11 +213,11 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin
diagnostic.range(),
)));
context.push_diagnostic(diagnostic);
} else if iter
.peek()
.is_some_and(|token| token.kind() == TokenKind::Rsqb)
{
} else if iter.peek().is_some_and(|token| {
matches!(token.kind(), TokenKind::Rsqb | TokenKind::Comma)
}) {
// Allow `foo[1 :]`, but not `foo[1 :]`.
// Or `foo[index :, 2]`, but not `foo[index :, 2]`.
if let (Whitespace::Many | Whitespace::Tab, offset) = whitespace
{
let mut diagnostic = Diagnostic::new(

View File

@@ -17,6 +17,9 @@ use crate::checkers::ast::Checker;
/// `sys.path.insert`, `sys.path.append`, and similar modifications between import
/// statements.
///
/// In [preview], this rule also allows `os.environ` modifications between import
/// statements.
///
/// ## Example
/// ```python
/// "One string"
@@ -37,6 +40,7 @@ use crate::checkers::ast::Checker;
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct ModuleImportNotAtTopOfFile {
source_type: PySourceType,

View File

@@ -86,30 +86,33 @@ pub(crate) fn trailing_whitespace(
.sum();
if whitespace_len > TextSize::from(0) {
let range = TextRange::new(line.end() - whitespace_len, line.end());
// Removing trailing whitespace is not safe inside multiline strings.
let applicability = if indexer.multiline_ranges().contains_range(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
if range == line.range() {
if settings.rules.enabled(Rule::BlankLineWithWhitespace) {
let mut diagnostic = Diagnostic::new(BlankLineWithWhitespace, range);
// Remove any preceding continuations, to avoid introducing a potential
// syntax error.
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
indexer
.preceded_by_continuations(line.start(), locator)
.unwrap_or(range.start()),
range.end(),
))));
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(TextRange::new(
indexer
.preceded_by_continuations(line.start(), locator)
.unwrap_or(range.start()),
range.end(),
)),
applicability,
));
return Some(diagnostic);
}
} else if settings.rules.enabled(Rule::TrailingWhitespace) {
let mut diagnostic = Diagnostic::new(TrailingWhitespace, range);
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(range),
// Removing trailing whitespace is not safe inside multiline strings.
if indexer.multiline_ranges().contains_range(range) {
Applicability::Unsafe
} else {
Applicability::Safe
},
applicability,
));
return Some(diagnostic);
}

View File

@@ -231,6 +231,8 @@ E20.py:149:10: E203 [*] Whitespace before ':'
148 | #: E203:1:10
149 | ham[upper :]
| ^^ E203
150 |
151 | #: Okay
|
= help: Remove whitespace before ':'
@@ -240,5 +242,21 @@ E20.py:149:10: E203 [*] Whitespace before ':'
148 148 | #: E203:1:10
149 |-ham[upper :]
149 |+ham[upper:]
150 150 |
151 151 | #: Okay
152 152 | ham[lower +1 :, "columnname"]
E20.py:155:14: E203 [*] Whitespace before ':'
|
154 | #: E203:1:13
155 | ham[lower + 1 :, "columnname"]
| ^^ E203
|
= help: Remove whitespace before ':'
Safe fix
152 152 | ham[lower +1 :, "columnname"]
153 153 |
154 154 | #: E203:1:13
155 |-ham[lower + 1 :, "columnname"]
155 |+ham[lower + 1:, "columnname"]

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402_2.py:7:1: E402 Module level import not at top of file
|
5 | del os.environ["WORLD_SIZE"]
6 |
7 | import torch
| ^^^^^^^^^^^^ E402
|

View File

@@ -11,7 +11,7 @@ W293.py:4:1: W293 [*] Blank line contains whitespace
|
= help: Remove whitespace from blank line
Safe fix
Unsafe fix
1 1 | # See: https://github.com/astral-sh/ruff/issues/9323
2 2 | class Chassis(RobotModuleTemplate):
3 3 | """底盘信息推送控制
@@ -48,6 +48,7 @@ W293.py:16:1: W293 [*] Blank line contains whitespace
15 | \
16 |
| ^^^^ W293
17 | '''blank line with whitespace
|
= help: Remove whitespace from blank line
@@ -59,5 +60,25 @@ W293.py:16:1: W293 [*] Blank line contains whitespace
15 |- \
16 |-
14 |+ "
17 15 | '''blank line with whitespace
18 16 |
19 17 | inside a multiline string'''
W293.py:18:1: W293 [*] Blank line contains whitespace
|
17 | '''blank line with whitespace
18 |
| ^ W293
19 | inside a multiline string'''
|
= help: Remove whitespace from blank line
Unsafe fix
15 15 | \
16 16 |
17 17 | '''blank line with whitespace
18 |-
18 |+
19 19 | inside a multiline string'''

View File

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

View File

@@ -87,12 +87,17 @@ impl Violation for IndentWithSpaces {
/// """
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// ## References
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [PEP 257]: https://peps.python.org/pep-0257/
/// [formatter]: https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct UnderIndentation;

View File

@@ -1665,21 +1665,30 @@ fn common_section(
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
let del_len = if num_blank_lines == 1 {
// SAFETY: Guaranteed to not be None, because `num_blank_lines`is 1.
context.following_lines().next_back().unwrap().text_len()
} else {
TextSize::new(0)
};
let edit = Edit::replacement(
format!(
"{}{}",
line_end.repeat(2 - num_blank_lines),
docstring.indentation
),
context.end() - del_len,
context.end(),
);
let mut diagnostic = Diagnostic::new(
BlankLineAfterLastSection {
name: context.section_name().to_string(),
},
docstring.range(),
);
// Add a newline after the section.
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
format!(
"{}{}",
line_end.repeat(2 - num_blank_lines),
docstring.indentation
),
context.end(),
)));
diagnostic.set_fix(Fix::safe_edit(edit));
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -27,10 +27,16 @@ use crate::docstrings::Docstring;
/// """Return the pathname of the KOS root directory."""
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent quotes, making the rule redundant.
///
/// ## References
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [formatter]: https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct TripleSingleQuotes {
expected_quote: Quote,

View File

@@ -46,7 +46,7 @@ D413.py:13:5: D413 [*] Missing blank line after last section ("Returns")
18 18 |
19 19 | Returns:
20 20 | the value
21 |+
21 |+
21 22 | """
22 23 |
23 24 |
@@ -75,5 +75,31 @@ D413.py:52:5: D413 [*] Missing blank line after last section ("Returns")
59 |+ the value
60 |+
61 |+ """
60 62 |
61 63 |
62 64 | def func():
D413.py:63:5: D413 [*] Missing blank line after last section ("Returns")
|
62 | def func():
63 | """Do something.
| _____^
64 | |
65 | | Args:
66 | | x: the value
67 | | with a hanging indent
68 | |
69 | | Returns:
70 | | the value
71 | | """
| |___________^ D413
|
= help: Add blank line after "Returns"
Safe fix
68 68 |
69 69 | Returns:
70 70 | the value
71 |- """
71 |+
72 |+ """

View File

@@ -44,7 +44,7 @@ sections.py:120:5: D413 [*] Missing blank line after last section ("Returns")
122 122 | Returns
123 123 | -------
124 124 | A value of some sort.
125 |+
125 |+
125 126 | """
126 127 |
127 128 |
@@ -67,7 +67,7 @@ sections.py:170:5: D413 [*] Missing blank line after last section ("Returns")
171 171 |
172 172 | Returns
173 173 | -------
174 |+
174 |+
174 175 | """
175 176 |
176 177 |
@@ -89,7 +89,7 @@ sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters")
520 520 |
521 521 | Parameters
522 522 | ==========
523 |+
523 |+
523 524 | """
524 525 |
525 526 |
@@ -111,7 +111,7 @@ sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters")
528 528 |
529 529 | Parameters
530 530 | ===========
531 |+
531 |+
531 532 | """
532 533 |
533 534 |
@@ -135,7 +135,7 @@ sections.py:548:5: D413 [*] Missing blank line after last section ("Args")
551 551 | Here's a note.
552 552 |
553 553 | returns:
554 |+
554 |+
554 555 | """
555 556 |
556 557 |
@@ -159,7 +159,7 @@ sections.py:558:5: D413 [*] Missing blank line after last section ("Returns")
561 561 | Here's a note.
562 562 |
563 563 | Returns:
564 |+
564 |+
564 565 | """
565 566 |
566 567 |
@@ -185,9 +185,7 @@ sections.py:588:5: D413 [*] Missing blank line after last section ("Parameters")
593 593 | A list of string parameters
594 594 | value:
595 595 | Some value
596 |+
596 |+
596 597 | """
597 598 |
598 599 |
598 599 |

View File

@@ -171,6 +171,7 @@ mod tests {
#[test_case(Rule::PotentialIndexError, Path::new("potential_index_error.py"))]
#[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))]
#[test_case(Rule::TooManyNestedBlocks, Path::new("too_many_nested_blocks.py"))]
#[test_case(Rule::DictIterMissingItems, Path::new("dict_iter_missing_items.py"))]
#[test_case(
Rule::UnnecessaryDictIndexLookup,
Path::new("unnecessary_dict_index_lookup.py")

View File

@@ -0,0 +1,110 @@
use ruff_python_ast::{Expr, ExprTuple};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_dict;
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for dictionary unpacking in a for loop without calling `.items()`.
///
/// ## Why is this bad?
/// When iterating over a dictionary in a for loop, if a dictionary is unpacked
/// without calling `.items()`, it could lead to a runtime error if the keys are not
/// a tuple of two elements.
///
/// It is likely that you're looking for an iteration over (key, value) pairs which
/// can only be achieved when calling `.items()`.
///
/// ## Example
/// ```python
/// data = {"Paris": 2_165_423, "New York City": 8_804_190, "Tokyo": 13_988_129}
///
/// for city, population in data:
/// print(f"{city} has population {population}.")
/// ```
///
/// Use instead:
/// ```python
/// data = {"Paris": 2_165_423, "New York City": 8_804_190, "Tokyo": 13_988_129}
///
/// for city, population in data.items():
/// print(f"{city} has population {population}.")
/// ```
#[violation]
pub struct DictIterMissingItems;
impl AlwaysFixableViolation for DictIterMissingItems {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unpacking a dictionary in iteration without calling `.items()`")
}
fn fix_title(&self) -> String {
format!("Add a call to `.items()`")
}
}
pub(crate) fn dict_iter_missing_items(checker: &mut Checker, target: &Expr, iter: &Expr) {
let Expr::Tuple(ExprTuple { elts, .. }) = target else {
return;
};
if elts.len() != 2 {
return;
};
let Some(name) = iter.as_name_expr() else {
return;
};
let Some(binding) = checker
.semantic()
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
// If we can reliably determine that a dictionary has keys that are tuples of two we don't warn
if is_dict_key_tuple_with_two_elements(checker.semantic(), binding) {
return;
}
let mut diagnostic = Diagnostic::new(DictIterMissingItems, iter.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{}.items()", name.id),
iter.range(),
)));
checker.diagnostics.push(diagnostic);
}
/// Returns true if the binding is a dictionary where each key is a tuple with two elements.
fn is_dict_key_tuple_with_two_elements(semantic: &SemanticModel, binding: &Binding) -> bool {
let Some(statement) = binding.statement(semantic) else {
return false;
};
let Some(assign_stmt) = statement.as_assign_stmt() else {
return false;
};
let Some(dict_expr) = assign_stmt.value.as_dict_expr() else {
return false;
};
dict_expr.keys.iter().all(|elt| {
elt.as_ref().is_some_and(|x| {
if let Some(tuple) = x.as_tuple_expr() {
return tuple.elts.len() == 2;
}
false
})
})
}

View File

@@ -13,6 +13,7 @@ pub(crate) use compare_to_empty_string::*;
pub(crate) use comparison_of_constant::*;
pub(crate) use comparison_with_itself::*;
pub(crate) use continue_in_finally::*;
pub(crate) use dict_iter_missing_items::*;
pub(crate) use duplicate_bases::*;
pub(crate) use empty_comment::*;
pub(crate) use eq_without_hash::*;
@@ -98,6 +99,7 @@ mod compare_to_empty_string;
mod comparison_of_constant;
mod comparison_with_itself;
mod continue_in_finally;
mod dict_iter_missing_items;
mod duplicate_bases;
mod empty_comment;
mod eq_without_hash;

View File

@@ -4,9 +4,10 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, DiagnosticKind, Edit,
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_trivia::indentation_at_offset;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix;
/// ## What it does
/// Checks for the use of a classmethod being made without the decorator.
@@ -86,21 +87,21 @@ enum MethodType {
}
/// PLR0202
pub(crate) fn no_classmethod_decorator(checker: &mut Checker, class_def: &ast::StmtClassDef) {
get_undecorated_methods(checker, class_def, &MethodType::Classmethod);
pub(crate) fn no_classmethod_decorator(checker: &mut Checker, stmt: &Stmt) {
get_undecorated_methods(checker, stmt, &MethodType::Classmethod);
}
/// PLR0203
pub(crate) fn no_staticmethod_decorator(checker: &mut Checker, class_def: &ast::StmtClassDef) {
get_undecorated_methods(checker, class_def, &MethodType::Staticmethod);
pub(crate) fn no_staticmethod_decorator(checker: &mut Checker, stmt: &Stmt) {
get_undecorated_methods(checker, stmt, &MethodType::Staticmethod);
}
fn get_undecorated_methods(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
method_type: &MethodType,
) {
let mut explicit_decorator_calls: HashMap<String, TextRange> = HashMap::default();
fn get_undecorated_methods(checker: &mut Checker, class_stmt: &Stmt, method_type: &MethodType) {
let Stmt::ClassDef(class_def) = class_stmt else {
return;
};
let mut explicit_decorator_calls: HashMap<String, &Stmt> = HashMap::default();
let (method_name, diagnostic_type): (&str, DiagnosticKind) = match method_type {
MethodType::Classmethod => ("classmethod", NoClassmethodDecorator.into()),
@@ -131,7 +132,7 @@ fn get_undecorated_methods(
if let Expr::Name(ast::ExprName { id, .. }) = &arguments.args[0] {
if target_name == *id {
explicit_decorator_calls.insert(id.clone(), stmt.range());
explicit_decorator_calls.insert(id.clone(), stmt);
}
};
}
@@ -151,7 +152,7 @@ fn get_undecorated_methods(
..
}) = stmt
{
if !explicit_decorator_calls.contains_key(name.as_str()) {
let Some(decorator_call_statement) = explicit_decorator_calls.get(name.as_str()) else {
continue;
};
@@ -177,18 +178,16 @@ fn get_undecorated_methods(
match indentation {
Some(indentation) => {
let range = &explicit_decorator_calls[name.as_str()];
// SAFETY: Ruff only supports formatting files <= 4GB
#[allow(clippy::cast_possible_truncation)]
diagnostic.set_fix(Fix::safe_edits(
Edit::insertion(
format!("@{method_name}\n{indentation}"),
stmt.range().start(),
),
[Edit::deletion(
range.start() - TextSize::from(indentation.len() as u32),
range.end(),
[fix::edits::delete_stmt(
decorator_call_statement,
Some(class_stmt),
checker.locator(),
checker.indexer(),
)],
));
checker.diagnostics.push(diagnostic);

View File

@@ -305,6 +305,7 @@ fn assignment_targets_from_expr<'a>(
ctx: ExprContext::Store,
elts,
range: _,
parenthesized: _,
}) => Box::new(
elts.iter()
.flat_map(|elt| assignment_targets_from_expr(elt, dummy_variable_rgx)),

View File

@@ -9,7 +9,9 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::hashable::HashableExpr;
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -74,7 +76,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
if bool_op
.values
.iter()
.any(|value| !is_allowed_value(bool_op.op, value))
.any(|value| !is_allowed_value(bool_op.op, value, checker.semantic()))
{
return;
}
@@ -143,6 +145,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})]),
range: bool_op.range(),
})),
@@ -157,7 +160,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
/// Return `true` if the given expression is compatible with a membership test.
/// E.g., `==` operators can be joined with `or` and `!=` operators can be
/// joined with `and`.
fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
fn is_allowed_value(bool_op: BoolOp, value: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Compare(ast::ExprCompare {
left,
ops,
@@ -196,6 +199,16 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
return false;
}
// Ignore `sys.version_info` and `sys.platform` comparisons, which are only
// respected by type checkers when enforced via equality.
if any_over_expr(value, &|expr| {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
})
}) {
return false;
}
true
}

View File

@@ -0,0 +1,43 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
dict_iter_missing_items.py:13:13: PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
12 | # Errors
13 | for k, v in d:
| ^ PLE1141
14 | pass
|
= help: Add a call to `.items()`
Safe fix
10 10 | s2 = {1, 2, 3}
11 11 |
12 12 | # Errors
13 |-for k, v in d:
13 |+for k, v in d.items():
14 14 | pass
15 15 |
16 16 | for k, v in d_tuple_incorrect_tuple:
dict_iter_missing_items.py:16:13: PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
14 | pass
15 |
16 | for k, v in d_tuple_incorrect_tuple:
| ^^^^^^^^^^^^^^^^^^^^^^^ PLE1141
17 | pass
|
= help: Add a call to `.items()`
Safe fix
13 13 | for k, v in d:
14 14 | pass
15 15 |
16 |-for k, v in d_tuple_incorrect_tuple:
16 |+for k, v in d_tuple_incorrect_tuple.items():
17 17 | pass
18 18 |
19 19 |

View File

@@ -23,9 +23,28 @@ no_method_decorator.py:9:5: PLR0202 [*] Class method defined without decorator
12 13 |
13 |- pick_colors = classmethod(pick_colors)
14 14 |
15 |+
15 16 | def pick_one_color(): # [no-staticmethod-decorator]
16 17 | """staticmethod to pick one fruit color"""
17 18 | return choice(Fruit.COLORS)
15 15 | def pick_one_color(): # [no-staticmethod-decorator]
16 16 | """staticmethod to pick one fruit color"""
no_method_decorator.py:22:5: PLR0202 [*] Class method defined without decorator
|
21 | class Class:
22 | def class_method(cls):
| PLR0202
23 | pass
|
= help: Add @classmethod decorator
Safe fix
19 19 | pick_one_color = staticmethod(pick_one_color)
20 20 |
21 21 | class Class:
22 |+ @classmethod
22 23 | def class_method(cls):
23 24 | pass
24 25 |
25 |- class_method = classmethod(class_method);another_statement
26 |+ another_statement
26 27 |
27 28 | def static_method():
28 29 | pass

View File

@@ -22,6 +22,27 @@ no_method_decorator.py:15:5: PLR0203 [*] Static method defined without decorator
17 18 | return choice(Fruit.COLORS)
18 19 |
19 |- pick_one_color = staticmethod(pick_one_color)
20 |+
20 20 |
21 21 | class Class:
22 22 | def class_method(cls):
no_method_decorator.py:27:5: PLR0203 [*] Static method defined without decorator
|
25 | class_method = classmethod(class_method);another_statement
26 |
27 | def static_method():
| PLR0203
28 | pass
|
= help: Add @staticmethod decorator
Safe fix
24 24 |
25 25 | class_method = classmethod(class_method);another_statement
26 26 |
27 |+ @staticmethod
27 28 | def static_method():
28 29 | pass
29 30 |
30 |- static_method = staticmethod(static_method);
31 |+

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