Compare commits

..

114 Commits

Author SHA1 Message Date
Zanie
f474920d87 Empty commit to test cache 2023-10-17 12:16:24 -05:00
Zanie
a556319e7f Use ci as shared key — os is encoded in the prefix already 2023-10-17 11:08:01 -05:00
Zanie
a29aa78702 Exclude the fuzzer 2023-10-17 11:05:09 -05:00
Zanie
7a16680791 Update rust-cache in CI to be shared across jobs
By default, the cache key includes the job identifier so we have a separate cache for each of our CI builds
2023-10-17 11:02:45 -05:00
Eiko Wagenknecht
88c0106421 [docs] fix typo (#8013)
## Summary

Fix a typo in the docs for quote style.

> a = "a string without any quotes"
> b = "It's monday morning"
> Ruff will change a to use single quotes when using quote-style =
"single". However, a will be unchanged, as converting to single quotes
would require the inner ' to be escaped, which leads to less readable
code: 'It\'s monday morning'.

It should read "However, **b** will be unchanged".

## Test Plan

N/A.
2023-10-17 14:16:28 +00:00
Zanie Blue
f60aa85471 Update GitHub actions example in docs to use --output-format (#8014) 2023-10-17 09:13:24 -05:00
Charlie Marsh
d942a777d7 Avoid flagging bad-dunder-method-name for _ (#8015)
This is almost certainly _not_ an accidentally mistyped dunder method.
Closes https://github.com/astral-sh/ruff/issues/8005.
2023-10-17 10:13:04 -04:00
Steve C
8a529925b3 Add autofix for D300 (#7967)
## Summary

Add fix for `D300`

## Test Plan

`cargo test` and manually
2023-10-17 09:37:46 -04:00
dependabot[bot]
dc6b4ad2b4 Bump tracing from 0.1.37 to 0.1.39 (#7978)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-10-17 07:46:53 +00:00
Steve C
21ea290d6a [pylint] Implement PLR0916 (too-many-boolean-expressions) (#7975)
## Summary

Add
[R0916](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/too-many-boolean-expressions.html),
no autofix available.

See: #970 

## Test Plan

`cargo test` and manually.
2023-10-17 04:44:25 +00:00
Steve C
5da0f9111e implement PLR6201 with autofix (#7973)
## Summary

Implements
[R6201](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/use-set-for-membership.html)
along with the autofix!

See #970 

## Test Plan

`cargo test`, and manually
2023-10-17 04:15:21 +00:00
Charlie Marsh
cb6d74c27b Use set bracket replacement for iteration-over-set (#8001) 2023-10-17 04:12:05 +00:00
Sven Hager
73049df3ed Implement the pylint rule PLW1514 (unspecified-encoding) (#7939)
## Summary

Implemented the pylint rule W1514 ( unspecified-encoding).
See also:
https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/unspecified-encoding.html


## Test Plan

Tested it with the submitted test case.
Additionally, we tested the new ruff rule (PLW1514) on our proprietary
Python code base.
2023-10-17 03:49:48 +00:00
Clément Schreiner
bf0e5788ef [pylint] Implement misplaced-bare-raise (E0704) (#7961)
## Summary

### What it does
This rule triggers an error when a bare raise statement is not in an
except or finally block.
### Why is this bad?
If raise statement is not in an except or finally block, there is no
active exception to
re-raise, so it will fail with a `RuntimeError` exception.
### Example
```python
def validate_positive(x):
   if x <= 0:
       raise
```
Use instead:
```python
def validate_positive(x):
   if x <= 0:
       raise ValueError(f"{x} is not positive")
```

## Test Plan

Added unit test and snapshot.
Manually compared ruff and pylint outputs on pylint's tests.

## References

- [pylint
documentation](https://pylint.pycqa.org/en/stable/user_guide/messages/error/misplaced-bare-raise.html)
- [pylint
implementation](https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/exceptions.py#L339)
2023-10-17 03:07:46 +00:00
Zanie Blue
4113d65836 Rename RuleGroup::Unspecified to Stable (#7991)
Should help with #7989 and seems more accurate for our new model
2023-10-16 14:53:27 -05:00
Clément Schreiner
4c2c9bf7e0 [docs] Clarify that new rules should be added to RuleGroup::Preview. (#7989)
In the contributing page, clarify that new rules must be added to
`RuleGroup::Preview` when mapping their code.
2023-10-16 15:14:09 -04:00
Zanie Blue
172ac2c9a2 Add entry for #7987 to 0.1.0 changelog (#7988) 2023-10-16 18:48:06 +00:00
Charlie Marsh
cac9754455 Update fix safety FAQ to reflect --unsafe-fixes (#7969) 2023-10-16 13:34:55 -05:00
Charlie Marsh
134def0119 Allow sunder names from enum.Enum (#7987)
Closes https://github.com/astral-sh/ruff/issues/7971.
2023-10-16 18:11:14 +00:00
Zanie Blue
1fabaca5de Bump version to 0.1.0 (#7931)
[Rendered
changelog](https://github.com/astral-sh/ruff/blob/release/010/CHANGELOG.md)
2023-10-16 13:06:48 -05:00
Zanie Blue
523f542dbd Remove support for providing output format via format option (#7984)
See the provided breaking changes note for details.

Removes support for the deprecated `--format`option in the `ruff check`
CLI, `format` inference as `output-format` in the configuration file,
and the `RUFF_FORMAT` environment variable.

The error message for use of `format` in the configuration file could be
better, but would require some awkward serde wrappers and it seems hard
to present the correct schema to the user still.
2023-10-16 13:06:12 -05:00
Charlie Marsh
ee7575eb5a Bump regex to 1.10.2 (#7985)
Recreating https://github.com/astral-sh/ruff/pull/7980 with regex's
latest fix.
2023-10-16 13:03:04 -04:00
Charlie Marsh
84f7391cc5 Use Cow in printf rewrite rule (#7986)
Small thing that bothered me when looking into the regex update.
2023-10-16 16:47:03 +00:00
dependabot[bot]
7da4e28a98 Bump aho-corasick from 1.1.1 to 1.1.2 (#7979) 2023-10-16 09:33:22 -04:00
dependabot[bot]
5718df638f Bump cloudflare/wrangler-action from 3.2.0 to 3.3.1 (#7982) 2023-10-16 09:32:15 -04:00
konsti
4bb4cd3b37 Update and extend formatter ecosystem checks (#7981)
**Summary** Adds home-assistant, a project with 10k files, and poetry,
which uses preview style, to the ecosystem checks.

Update all revisions to latest main.

Old:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76047 | 1789 | 1632 |
| django | 0.99983 | 2760 | 36 |
| transformers | 0.99963 | 2587 | 319 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99967 | 648 | 15 |
| zulip | 0.99972 | 1437 | 21 |


New:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.76382 | 1799 | 1436 |
| django | 0.99983 | 2772 | 31 |
| home-assistant | 0.99950 | 10596 | 165 |
| poetry | 0.99944 | 317 | 8 |
| transformers | 0.99961 | 2657 | 295 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99974 | 3669 | 19 |
| warehouse | 0.99971 | 654 | 13 |
| zulip | 0.99972 | 1459 | 13 |
2023-10-16 13:24:42 +02:00
konsti
620426de7a Use released unicode_name2 1.2.0 (#7983)
We can remove the git dependency (again). See
https://github.com/progval/unicode_names2/pull/34#issuecomment-1763141541

I'll run the release pipeline before merging.
2023-10-16 11:02:14 +02:00
dependabot[bot]
84ec66a22c Bump semver from 1.0.19 to 1.0.20 (#7977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 08:44:58 +00:00
dependabot[bot]
e58ffa9a7a Bump insta from 1.33.0 to 1.34.0 (#7976)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 08:44:38 +00:00
Charlie Marsh
aa6846c78c Add trailing zero between dot and exponential (#7956)
Closes https://github.com/astral-sh/ruff/issues/7952.
2023-10-15 21:42:00 -04:00
Charlie Marsh
3d03e75a9d Force parentheses for power operations in unary expressions (#7955)
## Summary

E.g., given `-10**100`, reformat as `-(10**100)`.

Black special cases this (https://github.com/psf/black/pull/909) and
it's currently a deviation.

Closes https://github.com/astral-sh/ruff/issues/7951.
2023-10-15 21:41:50 -04:00
Charlie Marsh
b6e75e58c9 Treat type aliases as typing-only expressions (#7968)
## Summary

Given `type RecordOrThings = Record | int | str`, the right-hand side
won't be evaluated at runtime. Same goes for `Record` in `type
RecordCallback[R: Record] = Callable[[R], None]`. This PR modifies the
visitation logic to treat them as typing-only.

Closes https://github.com/astral-sh/ruff/issues/7966.
2023-10-16 00:09:37 +00:00
Charlie Marsh
8061894af6 Resolve cache-dir relative to project root (#7962)
## Summary

Unlike other filepath-based settings, the `cache-dir` wasn't being
resolved relative to the project root, when specified as an absolute
path.

Closes https://github.com/astral-sh/ruff/issues/7958.
2023-10-14 19:00:23 +00:00
Victor Hugo Gomes
e261eb7461 Fix false positive in PLR6301 (#7933)
## Summary

Don't report a diagnostic if the method contains a `super()` call.

Closes #6961

## Test Plan

`cargo test`
2023-10-14 14:55:38 -04:00
Charlie Marsh
bd06cbe0c5 Respect subscripted base classes in type-checking rules (#7954)
Closes https://github.com/astral-sh/ruff/issues/7945.
2023-10-13 19:44:16 +00:00
Zanie Blue
ddffadb4b0 When only unsafe fixes are available, include note that no fixes are available first (#7950)
I believe this is a bit clearer.

When no fixes are available (safe _and_ unsafe) we will not include a
message at all.
2023-10-13 12:43:13 -05:00
Zanie Blue
8255e4ed6c Revert "add autofix for PYI030" (#7943)
This reverts commit #7880 (d8c0360fc7)
which does not perform the correct fix per
https://github.com/astral-sh/ruff/pull/7934
2023-10-13 09:24:47 -05:00
konsti
60ca6885b1 Fix i686 host builds (#7935)
See https://github.com/progval/unicode_names2/pull/34 for a detailed
explanation.

Fixes
https://github.com/astral-sh/ruff/actions/runs/6500014395/job/17659540717
and should unblock https://github.com/astral-sh/astral-sh/pull/44
2023-10-13 10:24:58 +02:00
Zanie Blue
889117ea87 Fix handling of Applicability::Display fixes when generating summary messages (#7932)
We were including `Display` fixes in the summary counts for unapplicable
fixes resulting in incorrect prompts to the user that a fix could be
enabled.
2023-10-12 20:33:31 -05:00
Jake Park
c03a693ebc [pylint] Implement consider-using-ternary (R1706) (#7811)
This is my first PR. Please feel free to give me any feedback for even
small drawbacks.

## Summary

Checks if pre-python 2.5 ternary syntax is used.

Before
```python
x, y = 1, 2
maximum = x >= y and x or y  # [consider-using-ternary]
```

After
```python
x, y = 1, 2
maximum = x if x >= y else y
```

References: 

[pylint](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/consider-using-ternary.html)
#970 
[and_or_ternary distinction
logic](https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/refactoring/refactoring_checker.py#L1813)

## Test Plan

Unit test, python file, snapshot added.
2023-10-13 01:29:19 +00:00
Harutaka Kawamura
6f9c317aa5 Simplify key in dct and dct[key] to dct.get(key) (#7895)
## Summary

Close #5933

## Test Plan

`cargo test`
2023-10-13 01:08:52 +00:00
Dhruv Manilawala
66179af4f1 Add cell field to JSON output format (#7664)
## Summary

This PR adds a new `cell` field to the JSON output format which
indicates the Notebook cell this diagnostic (and fix) belongs to. It
also updates the location for the diagnostic and fixes as per the
`NotebookIndex`. It will be used in the VSCode extension to display the
diagnostic in the correct cell.

The diagnostic and edit start and end source locations are translated
for the notebook as per the `NotebookIndex`. The end source location for
an edit needs some special handling.

### Edit end location

To understand this, the following context is required:

1. Visible lines in Jupyter Notebook vs JSON array strings: The newline
is part of the string in the JSON format. This means that if there are 3
visible lines in a cell where the last line is empty then the JSON would
contain 2 strings in the source array, both ending with a newline:

**JSON format:**
```json
[
	"# first line\n",
	"# second line\n",
]
```

**Notebook view:**
```python
1 # first line
2 # second line
3
```

2. If an edit needs to remove an entire line including the newline, then
the end location would be the start of the next row.

To remove a statement in the following code:
```python
import os
```

The edit would be:
```
start: row 1, col 1
end: row 2, col 1
```

Now, here's where the problem lies. The notebook index doesn't have any
information for row 2 because it doesn't exists in the actual notebook.
The newline was added by Ruff to concatenate the source code and it's
removed before writing back. But, the edit is computed looking at that
newline.

This means that while translating the end location for an edit belong to
a Notebook, we need to check if both the start and end location belongs
to the same cell. If not, then the end location should be the first
character of the next row and if so, translate that back to the last
character of the previous row. Taking the above example, the translated
location for Notebook would be:
```
start: row 1, col 1
end: row 1, col 10
```

## Test Plan

Add test cases for notebook output in the JSON format and update
existing snapshots.
2023-10-13 01:06:02 +00:00
Steve C
1e184e69f3 Add autofix for PYI055 (#7886) 2023-10-13 00:56:34 +00:00
Dhruv Manilawala
f08a5f67eb Add test for Notebook text output (#7925)
## Summary

This PR adds test cases for the Notebook output in text format.

## Test Plan

Update test snapshots.
2023-10-13 06:24:12 +05:30
Dhruv Manilawala
cd564c4200 Use OneIndexed in NotebookIndex (#7921)
## Summary

This PR refactors the `NotebookIndex` struct to use `OneIndexed` to make
the
intent of the code clearer.

## Test Plan

Update the existing test case and run `cargo test` to verify the change.

- [x] Verify `--diff` output
- [x] Verify the diagnostics output
- [x] Verify `--show-source` output
2023-10-13 06:23:49 +05:30
Zanie Blue
c1fdb9c46d Add unsafe fixes entry to breaking changes (#7930) 2023-10-12 17:35:39 -05:00
Zanie Blue
48b256bd94 Add breaking changes entry #7900 (#7928) 2023-10-12 16:01:50 +00:00
konsti
3944c42d4c Format comment before parameter default correctly (#7870)
**Summary** Handle comment before the default values of function
parameters correctly by inserting a line break instead of space after
the equals sign where required.

```python
def f(
    a = # parameter trailing comment; needs line break
    1,
    b =
    # default leading comment; needs line break
    2,
    c = ( # the default leading can only be end-of-line with parentheses; no line break
        3
    ),
    d = (
        # own line leading comment with parentheses; no line break
        4
    )
)
```

Fixes #7603

**Test Plan** Added the different cases and one more complex case as
fixtures.
2023-10-12 17:50:12 +02:00
Zanie Blue
cb06b7956c Add versioning policy to documentation (#7923)
Most of the content adapted from
https://github.com/astral-sh/ruff/discussions/6998
2023-10-12 10:42:35 -05:00
Dhruv Manilawala
4454fbf7e5 Fix E251 false positive inside f-strings (#7894)
## Summary

This PR fixes the bug where the rule `E251` was being triggered on a equal token
inside a f-string which was used in the context of debug expressions.

For example, the following was being flagged before the fix:

```python
print(f"{foo = }")
```

But, now it is not. This leads to false negatives such as:

```python
print(f"{foo(a = 1)}")
```

One solution would be to know if the opened parentheses was inside a f-string or
not. If it was then we can continue flagging until it's closed. If not, then we
should not flag it.

## Test Plan

Add new test cases and check that they don't raise any false positives.

fixes: #7882
2023-10-12 05:26:39 +00:00
Zanie Blue
b243840e4b Add an example of an unsafe fix (#7924)
Per review in #7901 adds an example of an unsafe fix.
2023-10-11 21:14:34 +00:00
Zanie Blue
23bbe7336a Fix false negative in outdated-version-block when using greater than comparisons (#7920)
Closes #7902
2023-10-11 14:33:43 -05:00
Steve C
a71c4dfabb fix edge case with PIE804 (#7922)
## Summary

`foo(**{})` was an overlooked edge case for `PIE804` which introduced a
crash within the Fix, introduced in #7884.

I've made it so that `foo(**{})` turns into `foo()` when applied with
`--fix`, but is that desired/expected? 🤔 Should we just ignore instead?

## Test Plan

`cargo test`
2023-10-11 15:05:49 -04:00
Zanie Blue
81275d12e9 Add documentation for fixes (#7901)
Adds documentation for using `ruff check . --fix`

Uses the draft of the "Automatic fixes" section from
https://github.com/astral-sh/ruff/pull/7732 and adds documentation for
unsafe fixes, applicability levels, and
https://github.com/astral-sh/ruff/pull/7841

I enabled admonitions because they're nice. We should use them more.
2023-10-11 16:41:17 +00:00
Zanie Blue
40cad44f4a Drop formatting specific rules from the default set (#7900)
Closes https://github.com/astral-sh/ruff/issues/7572

Drops formatting specific rules from the default rule set as they
conflict with formatters in general (and in particular, conflict with
our formatter). Most of these rules are in preview, but the removal of
`line-too-long` and `mixed-spaces-and-tabs` is a change to the stable
rule set.

## Example

The following no longer raises `E501`
```
echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = 1" | ruff check -
```
2023-10-11 11:29:34 -05:00
Charlie Marsh
c38617fa27 Remove per-diagnostic check for fixability (#7919)
## Summary

Throughout the codebase, we have this pattern:

```rust
let mut diagnostic = ...
if checker.patch(Rule::UnusedVariable) {
    // Do the fix.
}
diagnostics.push(diagnostic)
```

This was helpful when we computed fixes lazily; however, we now compute
fixes eagerly, and this is _only_ used to ensure that we don't generate
fixes for rules marked as unfixable.

We often forget to add this, and it leads to bugs in enforcing
`--unfixable`.

This PR instead removes all of these checks, moving the responsibility
of enforcing `--unfixable` up to `check_path`. This is similar to how
@zanieb handled the `--extend-unsafe` logic: we post-process the
diagnostics to remove any fixes that should be ignored.
2023-10-11 16:09:47 +00:00
Steve C
1835d7bb45 add autofix for PLR1714 (#7910)
## Summary

Add autofix for `PLR1714` using tuples.

If added complexity is desired, we can lean into the `set` part by doing
some kind of builtin check on all of the comparator elements for
starters, since we otherwise don't know if something's hashable.

## Test Plan

`cargo test`, and manually.
2023-10-11 14:42:21 +00:00
Harutaka Kawamura
f670f9b22c [SIM115] Allow open followed by close (#7916) 2023-10-11 13:53:48 +00:00
Charlie Marsh
7a072cc2ea Respect --unfixable in ISC rules (#7917)
Closes https://github.com/astral-sh/ruff/issues/7909.
2023-10-11 13:40:24 +00:00
Harutaka Kawamura
8c4b5d3c90 Visit pattern match guard as a boolean test (#7911) 2023-10-11 09:26:10 -04:00
konsti
ec9d5cddd6 Less scary ruff format message (#7867) 2023-10-11 11:46:41 +00:00
konsti
0f759af3cf Remove spaces from import statements (#7859)
**Summary** Remove spaces from import statements such as 

```python
import tqdm .  tqdm
from tqdm .    auto import tqdm
```

See also #7760 for a better solution.

**Test Plan** New fixtures
2023-10-11 11:35:41 +00:00
konsti
644011fb14 Formatter quoting for f-strings with triple quotes (#7826)
**Summary** Quoting of f-strings can change if they are triple quoted
and only contain single quotes inside.

Fixes #6841

**Test Plan** New fixtures

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2023-10-11 11:30:34 +00:00
T-256
a1ee6d28ce UP018: Improve Fix message (#7913)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
closes https://github.com/astral-sh/ruff/issues/7912

## Test Plan

<!-- How was it tested? -->
2023-10-11 09:41:51 +00:00
Steve C
826868da5b add autofix for PIE804 (#7884) 2023-10-11 01:07:34 +00:00
Steve C
5986ff748a add autofix for PLC0208 (#7887) 2023-10-11 00:48:10 +00:00
Zanie Blue
739a8aa10e Add settings for promoting and demoting fixes (#7841)
Adds two configuration-file only settings `extend-safe-fixes` and
`extend-unsafe-fixes` which can be used to promote and demote the
applicability of fixes for rules.

Fixes with `Never` applicability cannot be promoted.
2023-10-10 20:04:21 +00:00
Charlie Marsh
090c1a4a19 Avoid converting f-strings within Django gettext calls (#7898)
## Summary

Django's `gettext` doesn't support f-strings, so we should avoid
translating `.format` calls in those cases.

Closes https://github.com/astral-sh/ruff/issues/7891.
2023-10-10 16:31:09 +00:00
Zanie Blue
2b95d3832b Update fix summary message in check --diff to include unsafe fix hints (#7790)
Requires #7769 

Updates the CLI display for `ruff check --fix` to hint availability of
unsafe fixes.

 ```
❯ ruff check example.py --select F601,T201 --diff --no-cache
No errors fixed (1 fix available with `--unsafe-fixes`).
```

```
❯ ruff check example.py --select F601,T201,W292 --diff --no-cache
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
 x = {'a': 1, 'a': 1}
-print(('foo'))
+print(('foo'))
\ No newline at end of file

Would fix 1 error (1 additional fix available with `--unsafe-fixes`).
```
```
❯ ruff check example.py --select F601,T201,W292 --diff --no-cache
--unsafe-fixes
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print(('foo'))
+x = {'a': 1, 'a': 1}
+print(('foo'))
\ No newline at end of file

Would fix 2 errors.
```
2023-10-10 10:50:35 -05:00
Philipp A
d412e8ef74 [docs] Default to following the system dark/light mode (#7888)
## Summary

This fixes the theme toggle so it has the most useful setting on by
default: following the system dark/light mode

See
https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#automatic-light-dark-mode

## Test Plan

It follows the official docs exactly. If the docs get published for each
PR branch, we can also check there if it works as intended.

---------

Co-authored-by: konstin <konstin@mailbox.org>
2023-10-10 15:25:49 +00:00
Dhruv Manilawala
46e45bdf19 Upgrade LibCST to 1.1.0 (#7896)
This PR updates the `libcst` crate version to `1.1.0`. The published
version contains support for 3.12:
https://github.com/Instagram/LibCST/releases/tag/v1.1.0
2023-10-10 14:58:03 +00:00
Charlie Marsh
a3e8e77172 Allow bindings to be created and referenced within annotations (#7885)
## Summary

Given:

```python
baz: Annotated[
    str,
    [qux for qux in foo],
]
```

We treat `baz` as `BindingKind::Annotation`, to ensure that references
to `baz` are marked as unbound. However, we were _also_ treating `qux`
as `BindingKind::Annotation`, which meant that the load in the
comprehension _also_ errored.

Closes https://github.com/astral-sh/ruff/issues/7879.
2023-10-10 03:51:00 +00:00
Charlie Marsh
ec7395ba69 Promote some rules to "always" fixable (#7840)
## Summary

This PR upgrades some rules from "sometimes" to "always" fixes, now that
we're getting ready to ship support in the CLI. The focus here was on
identifying rules for which the diagnostic itself is high-confidence,
and the fix itself is too (assuming that the diagnostic is correct).
This is _unlike_ rules that _may_ be a false positive, like those that
(e.g.) assume an object is a dictionary when you call `.values()` on it.

Specifically, I upgraded:

- A bunch of rules that only apply to `.pyi` files.
- Rules that rewrite deprecated imports or aliases.
- Some other misc. rules, like: `empty-print-string`, `unused-noqa`,
`getattr-with-constant`.

Open to feedback on any of these.
2023-10-10 03:30:46 +00:00
Steve C
d8c0360fc7 add autofix for PYI030 (#7880)
## Summary

Adds autofix to `PYI030`

Closes #7854. 

Unsure if the cloning method I chose is the best solution here, feel
free to suggest alternatives!

## Test Plan

`cargo test` as well as manually
2023-10-10 03:15:13 +00:00
Henry Schreiner
097b654ba7 fix(schema): restore limit on line-length (#7883)
## Summary

Restores functionality of #7875 but in the correct place. Closes #7877.

~~I couldn't figure out how to get cargo fmt to work, so hopefully
that's run in CI.~~ Nevermind, figured it out.

## Test Plan

Can see output of json.
2023-10-09 23:04:08 -04:00
Charlie Marsh
d54cabd276 Remove min and max range on line-length JSON schema (#7875)
## Summary

This was introduced in https://github.com/astral-sh/ruff/pull/7412, but
SchemaStore doesn't accept it. I manually edited the JSON schema last
time, then tried to fix this, then gave up -- so removing for now.

(See, e.g., https://github.com/SchemaStore/schemastore/pull/3278, which
failed prior to removing the min and max.)
2023-10-09 15:36:43 -04:00
Harutaka Kawamura
7faa43108f New rule: Prevent assignment expressions in assert statements (#7856) 2023-10-09 19:35:11 +00:00
Charlie Marsh
74b00c9b91 Fix commented-out coalesce keyword (#7876)
See:
https://github.com/astral-sh/ruff/pull/7874#issuecomment-1753498994.
2023-10-09 19:11:45 +00:00
Charlie Marsh
97e944003b Add sqlalchemy methods to boolean-trap exclusion list (#7874)
Closes https://github.com/astral-sh/ruff/issues/7869.
2023-10-09 18:50:51 +00:00
Alex Waygood
016e16254a Update UP038 docs to note that it results in slower code (#7872)
See discussion in #7871. I tried to use language similar to the existing
performance warnings in the `flake8-use-pathlib` docs, e.g.
https://docs.astral.sh/ruff/rules/os-path-abspath/#os-path-abspath-pth100
2023-10-09 11:45:14 -05:00
Charlie Marsh
61a41334a3 Show custom message for Path.joinpath with starred arguments (#7852)
Closes https://github.com/astral-sh/ruff/issues/7833.
2023-10-09 12:04:35 +00:00
dependabot[bot]
74971617a1 Bump cloudflare/wrangler-action from 3.1.1 to 3.2.0 (#7863) 2023-10-09 07:44:51 -04:00
dependabot[bot]
5c68c89566 Bump similar from 2.2.1 to 2.3.0 (#7862) 2023-10-09 07:44:42 -04:00
dependabot[bot]
8923eb19e0 Bump regex from 1.9.5 to 1.9.6 (#7861) 2023-10-09 07:44:35 -04:00
dependabot[bot]
dad70fff99 Bump syn from 2.0.37 to 2.0.38 (#7860) 2023-10-09 07:44:28 -04:00
dependabot[bot]
b72c94b3d1 Bump proc-macro2 from 1.0.67 to 1.0.69 (#7864) 2023-10-09 07:44:20 -04:00
dependabot[bot]
b4b296dca3 Bump clap from 4.4.5 to 4.4.6 (#7865) 2023-10-09 07:44:12 -04:00
Dhruv Manilawala
43883b7a15 Disallow f-strings in match pattern literal (#7857)
## Summary

This PR fixes a bug to disallow f-strings in match pattern literal.

```
literal_pattern ::=  signed_number
                     | signed_number "+" NUMBER
                     | signed_number "-" NUMBER
                     | strings
                     | "None"
                     | "True"
                     | "False"
                     | signed_number: NUMBER | "-" NUMBER
```

Source:
https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-literal_pattern

Also,

```console
$ python /tmp/t.py
  File "/tmp/t.py", line 4
    case "hello " f"{name}":
         ^^^^^^^^^^^^^^^^^^
SyntaxError: patterns may only match literals and attribute lookups
```

## Test Plan

Update existing test case and accordingly the snapshots. Also, add a new
test case to verify that the parser does raise an error.
2023-10-09 10:11:08 +00:00
bluthej
38f512d588 Fix diff (old and new were reversed) (#7855)
## Summary

Fixes #7853.

The old and new source files were reversed in the call to
`TextDiff::from_lines`, so the diff output of the CLI was also reversed.

## Test Plan

Two snapshots were updated in the process, so any reversal should be
caught :)
2023-10-09 12:58:13 +05:30
Zanie Blue
2d6557a51b Only show warnings for empty preview selectors when enabling rules (#7842)
Closes https://github.com/astral-sh/ruff/issues/7491

Users found it confusing that warnings were displayed when ignoring a
preview rule (which has no effect without `--preview`). While we could
retain the warning with different messaging, I've opted to remove it for
now. With this pull request, we will only warn on `--select` and
`--extend-select` but not `--fixable`, `--unfixable`, `--ignore`, or
`--extend-fixable`.
2023-10-08 11:14:37 -05:00
Simon Høxbro Hansen
2ba5677700 Improvements to RUF015 (#7848)
## Summary

Resolves https://github.com/astral-sh/ruff/issues/7618. 

The list of builtin iterator is not exhaustive.

## Test Plan

`cargo test`

``` python
a = [1, 2]

examples = [
    enumerate(a),
    filter(lambda x: x, a),
    map(int, a),
    reversed(a),
    zip(a),
    iter(a),
]

for example in examples:
    print(next(example))
```
2023-10-08 14:49:45 +00:00
Tom Kuson
62f1ee08e7 [refurb] Implement single-item-membership-test (FURB171) (#7815)
## Summary

Implement
[`no-single-item-in`](https://github.com/dosisod/refurb/blob/master/refurb/checks/iterable/no_single_item_in.py)
as `single-item-membership-test` (`FURB171`).

Uses the helper function `generate_comparison` from the `pycodestyle`
implementations; this function should probably be moved, but I am not
sure where at the moment.

Update: moved it to `ruff_python_ast::helpers`.

Related to #1348.

## Test Plan

`cargo test`
2023-10-08 14:08:47 +00:00
Chris Pryer
bdd925c0f2 Use workspace tracing in ruff_formatter crate (#7849)
I noticed that `tracing::instrument` wasn't available with only the
`"std"` feature enabled when trying to run `cargo doc -p
ruff_formatter`.

I could be misunderstanding something, but I couldn't even run the tests
for the crate.

```
ruff on  ruff-formatter-tracing [$] is 📦 v0.0.292 via 🦀 v1.72.0 
❯ cargo test -p ruff_formatter          
   Compiling ruff_formatter v0.0.0 (/Users/chrispryer/github/ruff/crates/ruff_formatter)
error[E0433]: failed to resolve: could not find `instrument` in `tracing`
   --> crates/ruff_formatter/src/printer/mod.rs:57:16
    |
57  |     #[tracing::instrument(name = "Printer::print", skip_all)]
    |                ^^^^^^^^^^ could not find `instrument` in `tracing`
    |
note: found an item that was configured out
   --> /Users/chrispryer/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tracing-0.1.37/src/lib.rs:959:29
    |
959 | pub use tracing_attributes::instrument;
    |                             ^^^^^^^^^^
    = note: the item is gated behind the `attributes` feature

For more information about this error, try `rustc --explain E0433`.
error: could not compile `ruff_formatter` (lib) due to previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `ruff_formatter` (lib test) due to previous error
```

Maybe the idea is to keep this crate minimal, but I figured I'd at least
point this out.
2023-10-08 09:50:10 -04:00
konsti
dd36a2516e Make serde a default feature of ruff_python_formatter (#7825)
This makes `cargo test -p ruff_python_formatter` actually run the tests
again
2023-10-08 09:47:13 -04:00
Chris Pryer
b6c9cf1c5b Update ruff_python_formatter generate.py comment (#7850)
I believe Docs.md is old.
2023-10-07 20:56:07 -04:00
Tom Kuson
805fd1bc93 Document reimplemented-starmap performance effects (#7846)
## Summary

Document the performance effects of `itertools.starmap`, including that
it is actually slower than comprehensions in Python 3.12.

Closes #7771.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-10-07 09:27:02 -04:00
Zanie Blue
0fc76ba276 Rename applicability levels to Safe, Unsafe, and Display (#7843)
After working with the previous change in
https://github.com/astral-sh/ruff/pull/7821 I found the names a bit
unclear and their relationship with the user-facing API muddied. Since
the applicability is exposed to the user directly in our JSON output, I
think it's important that these names align with our configuration
options. I've replaced `Manual` or `Never` with `Display` which captures
our intent for these fixes (only for display). Here, we create room for
future levels, such as `HasPlaceholders`, which wouldn't fit into the
`Always`/`Sometimes`/`Never` levels.

Unlike https://github.com/astral-sh/ruff/pull/7819, this retains the
flat enum structure which is easier to work with.
2023-10-06 20:50:05 -05:00
Zanie Blue
4b537d1297 Update non-pep695-type-alias to require --unsafe-fixes outside of stub files (#7836)
Closes https://github.com/astral-sh/ruff/issues/6434
2023-10-06 14:56:40 -05:00
Zanie Blue
3c25d261fe Write summary messages to stderr when fixing via stdin (instead of omitting them) (#7838)
Previously we just omitted diagnostic summaries when using `--fix` or
`--diff` with a stdin file. Now, we still write the summaries to stderr
instead of the main writer (which is generally stdout but could be
changed by `--output-file`).
2023-10-06 12:11:03 -05:00
Zanie Blue
4f95df1b6d Fixup use of deprecated --format option in warning (#7837) 2023-10-06 16:10:48 +00:00
Zanie Blue
22e18741bd Update CLI to respect fix applicability (#7769)
Rebase of https://github.com/astral-sh/ruff/pull/5119 authored by
@evanrittenhouse with additional refinements.

## Changes

- Adds `--unsafe-fixes` / `--no-unsafe-fixes` flags to `ruff check`
- Violations with unsafe fixes are not shown as fixable unless opted-in
- Fix applicability is respected now
    - `Applicability::Never` fixes are no longer applied
    - `Applicability::Sometimes` fixes require opt-in
    - `Applicability::Always` fixes are unchanged
- Hints for availability of `--unsafe-fixes` added to `ruff check`
output

## Examples

Check hints at hidden unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

We could add an indicator for which violations have hidden fixes in the
future.

Check treats unsafe fixes as applicable with opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --unsafe-fixes
example.py:1:14: F601 [*] Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 2 fixable with the --fix option.
```

Also can be enabled in the config file

```
❯ cat ruff.toml
unsafe-fixes = true
```

And opted-out per invocation

```
❯ ruff check example.py --no-cache --select F601,W292 --no-unsafe-fixes
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Diff does not include unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292 --diff
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
 x = {'a': 1, 'a': 1}
-print(('foo'))
+print(('foo'))
\ No newline at end of file

Would fix 1 error.
```

Unless there is opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --diff --unsafe-fixes
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print(('foo'))
+x = {'a': 1, 'a': 1}
+print(('foo'))
\ No newline at end of file

Would fix 2 errors.
```

https://github.com/astral-sh/ruff/pull/7790 will improve the diff
messages following this pull request

Similarly, `--fix` and `--fix-only` require the `--unsafe-fixes` flag to
apply unsafe fixes.

## Related

Replaces #5119
Closes https://github.com/astral-sh/ruff/issues/4185
Closes https://github.com/astral-sh/ruff/issues/7214
Closes https://github.com/astral-sh/ruff/issues/4845
Closes https://github.com/astral-sh/ruff/issues/3863
Addresses https://github.com/astral-sh/ruff/issues/6835
Addresses https://github.com/astral-sh/ruff/issues/7019
Needs follow-up https://github.com/astral-sh/ruff/issues/6962
Needs follow-up https://github.com/astral-sh/ruff/issues/4845
Needs follow-up https://github.com/astral-sh/ruff/issues/7436
Needs follow-up https://github.com/astral-sh/ruff/issues/7025
Needs follow-up https://github.com/astral-sh/ruff/issues/6434
Follow-up #7790 
Follow-up https://github.com/astral-sh/ruff/pull/7792

---------

Co-authored-by: Evan Rittenhouse <evanrittenhouse@gmail.com>
2023-10-06 03:41:43 +00:00
Zanie Blue
e8d2cbc3f6 Fix invalid code in FURB177 example (#7832) 2023-10-05 19:25:10 -05:00
Timo Brembeck
1dd5deb53d Fix typo in docs of PLR6301 (#7831)
## Summary
The example code for [PLR6301
(no-self-use)](https://docs.astral.sh/ruff/rules/no-self-use/#example)
contains f-strings without placeholder expressions, which is discouraged
according to [F541
(f-string-missing-placeholders)](https://docs.astral.sh/ruff/rules/f-string-missing-placeholders/).
For such a trivial change, I didn't open a separate issue.
2023-10-05 21:16:43 +00:00
Zanie Blue
b64f403dc2 Rename applicability levels to always, sometimes, and never (#7821)
Following much discussion for #4181 at
https://github.com/astral-sh/ruff/pull/5119,
https://github.com/astral-sh/ruff/discussions/5476, #7769,
https://github.com/astral-sh/ruff/pull/7819, and in
[Discord](https://discord.com/channels/1039017663004942429/1082324250112823306/1159144114231709746),
this pull request changes `Applicability` from using `Automatic`,
`Suggested`, and `Manual` to `Always`, `Sometimes`, and `Never`.

Also removes `Applicability::Unspecified` (replacing #7792).
2023-10-05 13:43:46 -05:00
Zanie Blue
7dc9887ab9 Remove unused empty file (#7830) 2023-10-05 13:35:50 -05:00
Dhruv Manilawala
709abd534a Fix lexing single-quoted f-string with multi-line format spec (#7787)
## Summary

Reported at https://github.com/python/cpython/issues/110259

## Test Plan

Add test cases for the fix and update the snapshots
2023-10-05 23:12:09 +05:30
Dhruv Manilawala
27def479bd Remove unused ts directive (#7829)
See: https://github.com/astral-sh/ruff/actions/runs/6421876406/job/17437188754
2023-10-05 17:11:05 +00:00
konsti
1eac457c1b Fix typo (#7828) 2023-10-05 16:56:11 +00:00
Charlie Marsh
609a78b13e Add trailing comment deviation to README (#7827)
Closes https://github.com/astral-sh/ruff/issues/7823.
2023-10-05 16:01:40 +00:00
Dhruv Manilawala
17fba99ed4 Report precise location for invalid conversion flag (#7809)
## Summary

This PR updates the parser definition to use the precise location when reporting
an invalid f-string conversion flag error.

Taking the following example code:
```python
f"{foo!x}"
```

On earlier version,
```
Error: f-string: invalid conversion character at byte offset 6
```

Now,
```
Error: f-string: invalid conversion character at byte offset 7
```

This becomes more useful when there's whitespace between `!` and the flag value
although that is not valid but we can't detect that now.

## Test Plan

As mentioned above.
2023-10-05 17:46:14 +05:30
Dhruv Manilawala
adb6580270 Fix playground Quick Fix action (#7824)
## Summary

This PR fixes the playground code action to start working again. It seems that
the field name was changed from `edit` to `textEdit` in some version.

Resources:
- https://microsoft.github.io/monaco-editor/docs.html#interfaces/languages.IWorkspaceTextEdit.html
- https://stackoverflow.com/a/71742764

## Test Plan

Tested it out running locally.
2023-10-05 11:41:18 +05:30
Cosmo
76fcf63052 Correct error in tuple example in ruff formatter docs (#7822)
## Summary

The fourth element should be "d" instead of "c" in the tuple example in
the ruff formatter docs.

## Test Plan

N/A
2023-10-04 22:51:17 +00:00
Charlie Marsh
90de108bfa Split up ast_if.rs into distinct rule files (#7820)
These node-centric rule files are too hard to navigate. Better to have a
single file per rule as we do elsewhere.
2023-10-04 19:39:05 +00:00
Charlie Marsh
ad265fa6bc Allow f-string modifications in line-shrinking cases (#7818)
## Summary

This PR resolves an issue raised in
https://github.com/astral-sh/ruff/discussions/7810, whereby we don't fix
an f-string that exceeds the line length _even if_ the resultant code is
_shorter_ than the current code.

As part of this change, I've also refactored and extracted some common
logic we use around "ensuring a fix isn't breaking the line length
rules".

## Test Plan

`cargo test`
2023-10-04 15:24:07 -04:00
609 changed files with 26850 additions and 21496 deletions

View File

@@ -77,6 +77,8 @@ jobs:
rustup component add clippy
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: "Clippy (wasm)"
@@ -97,6 +99,8 @@ jobs:
with:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Run tests (Ubuntu)"
if: ${{ matrix.os == 'ubuntu-latest' }}
run: cargo insta test --all --all-features --unreferenced reject
@@ -146,6 +150,8 @@ jobs:
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Run wasm-pack"
run: |
cd crates/ruff_wasm
@@ -159,6 +165,8 @@ jobs:
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
- run: cargo check
- run: cargo fmt --all --check
@@ -227,6 +235,8 @@ jobs:
# Only pinned to make caching work, update freely
run: rustup toolchain install nightly-2023-06-08
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
@@ -242,6 +252,8 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
@@ -267,6 +279,8 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Install pre-commit"
run: pip install pre-commit
- name: "Cache pre-commit"
@@ -300,6 +314,8 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: pip install -r docs/requirements-insiders.txt
@@ -352,6 +368,8 @@ jobs:
tool: cargo-codspeed
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci"
- name: "Build benchmarks"
run: cargo codspeed build --features codspeed -p ruff_benchmark

View File

@@ -47,7 +47,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.1
uses: cloudflare/wrangler-action@v3.3.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.1
uses: cloudflare/wrangler-action@v3.3.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -23,6 +23,7 @@ repos:
- id: mdformat
additional_dependencies:
- mdformat-mkdocs
- mdformat-admon
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.33.0

View File

@@ -1,5 +1,35 @@
# Breaking Changes
## 0.1.0
### The deprecated `format` setting has been removed
Ruff previously used the `format` setting, `--format` CLI option, and `RUFF_FORMAT` environment variable to
configure the output format of the CLI. This usage was deprecated in `v0.0.291` — the `format` setting is now used
to control Ruff's code formatting. As of this release:
- The `format` setting cannot be used to configure the output format, use `output-format` instead
- The `RUFF_FORMAT` environment variable is ignored, use `RUFF_OUTPUT_FORMAT` instead
- The `--format` option has been removed from `ruff check`, use `--output-format` instead
### Unsafe fixes are not applied by default ([#7769](https://github.com/astral-sh/ruff/pull/7769))
Ruff labels fixes as "safe" and "unsafe". The meaning and intent of your code will be retained when applying safe
fixes, but the meaning could be changed when applying unsafe fixes. Previously, unsafe fixes were always displayed
and applied when fixing was enabled. Now, unsafe fixes are hidden by default and not applied. The `--unsafe-fixes`
flag or `unsafe-fixes` configuration option can be used to enable unsafe fixes.
See the [docs](https://docs.astral.sh/ruff/configuration/#fix-safety) for details.
### Remove formatter-conflicting rules from the default rule set ([#7900](https://github.com/astral-sh/ruff/pull/7900))
Previously, Ruff enabled all implemented rules in Pycodestyle (`E`) by default. Ruff now only includes the
Pycodestyle prefixes `E4`, `E7`, and `E9` to exclude rules that conflict with automatic formatters. Consequently,
the stable rule set no longer includes `line-too-long` (`E501`) and `mixed-spaces-and-tabs` (`E101`). Other
excluded Pycodestyle rules include whitespace enforcement in `E1` and `E2`; these rules are currently in preview, and are already omitted by default.
This change only affects those using Ruff under its default rule set. Users that include `E` in their `select` will experience no change in behavior.
## 0.0.288
### Remove support for emoji identifiers ([#7212](https://github.com/astral-sh/ruff/pull/7212))

115
CHANGELOG.md Normal file
View File

@@ -0,0 +1,115 @@
# Changelog
This is the first release which uses the `CHANGELOG` file. See [GitHub Releases](https://github.com/astral-sh/ruff/releases) for prior changelog entries.
Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
## 0.1.0
### Breaking changes
- Unsafe fixes are no longer displayed or applied without opt-in ([#7769](https://github.com/astral-sh/ruff/pull/7769))
- Drop formatting specific rules from the default set ([#7900](https://github.com/astral-sh/ruff/pull/7900))
- The deprecated `format` setting has been removed ([#7984](https://github.com/astral-sh/ruff/pull/7984))
- The `format` setting cannot be used to configure the output format, use `output-format` instead
- The `RUFF_FORMAT` environment variable is ignored, use `RUFF_OUTPUT_FORMAT` instead
- The `--format` option has been removed from `ruff check`, use `--output-format` instead
### Rule changes
- Extend `reimplemented-starmap` (`FURB140`) to catch calls with a single and starred argument ([#7768](https://github.com/astral-sh/ruff/pull/7768))
- Improve cases covered by `RUF015` ([#7848](https://github.com/astral-sh/ruff/pull/7848))
- Update `SIM15` to allow `open` followed by `close` ([#7916](https://github.com/astral-sh/ruff/pull/7916))
- Respect `msgspec.Struct` default-copy semantics in `RUF012` ([#7786](https://github.com/astral-sh/ruff/pull/7786))
- Add `sqlalchemy` methods to \`flake8-boolean-trap\`\` exclusion list ([#7874](https://github.com/astral-sh/ruff/pull/7874))
- Add fix for `PLR1714` ([#7910](https://github.com/astral-sh/ruff/pull/7910))
- Add fix for `PIE804` ([#7884](https://github.com/astral-sh/ruff/pull/7884))
- Add fix for `PLC0208` ([#7887](https://github.com/astral-sh/ruff/pull/7887))
- Add fix for `PYI055` ([#7886](https://github.com/astral-sh/ruff/pull/7886))
- Update `non-pep695-type-alias` to require `--unsafe-fixes` outside of stub files ([#7836](https://github.com/astral-sh/ruff/pull/7836))
- Improve fix message for `UP018` ([#7913](https://github.com/astral-sh/ruff/pull/7913))
- Update `PLW3201` to support `Enum` [sunder names](https://docs.python.org/3/library/enum.html#supported-sunder-names) ([#7987](https://github.com/astral-sh/ruff/pull/7987))
### Preview features
- Only show warnings for empty preview selectors when enabling rules ([#7842](https://github.com/astral-sh/ruff/pull/7842))
- Add `unnecessary-key-check` to simplify `key in dct and dct[key]` to `dct.get(key)` ([#7895](https://github.com/astral-sh/ruff/pull/7895))
- Add `assignment-in-assert` to prevent walrus expressions in assert statements ([#7856](https://github.com/astral-sh/ruff/pull/7856))
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
### Configuration
- Add `unsafe-fixes` setting ([#7769](https://github.com/astral-sh/ruff/pull/7769))
- Add `extend-safe-fixes` and `extend-unsafe-fixes` for promoting and demoting fixes ([#7841](https://github.com/astral-sh/ruff/pull/7841))
### CLI
- Added `--unsafe-fixes` option for opt-in to display and apply unsafe fixes ([#7769](https://github.com/astral-sh/ruff/pull/7769))
- Fix use of deprecated `--format` option in warning ([#7837](https://github.com/astral-sh/ruff/pull/7837))
- Show changed files when running under `--check` ([#7788](https://github.com/astral-sh/ruff/pull/7788))
- Write summary messages to stderr when fixing via stdin instead of omitting them ([#7838](https://github.com/astral-sh/ruff/pull/7838))
- Update fix summary message in `check --diff` to include unsafe fix hints ([#7790](https://github.com/astral-sh/ruff/pull/7790))
- Add notebook `cell` field to JSON output format ([#7664](https://github.com/astral-sh/ruff/pull/7664))
- Rename applicability levels to `Safe`, `Unsafe`, and `Display` ([#7843](https://github.com/astral-sh/ruff/pull/7843))
### Bug fixes
- Fix bug where f-strings were allowed in match pattern literal ([#7857](https://github.com/astral-sh/ruff/pull/7857))
- Fix `SIM110` with a yield in the condition ([#7801](https://github.com/astral-sh/ruff/pull/7801))
- Preserve trailing comments in `C414` fixes ([#7775](https://github.com/astral-sh/ruff/pull/7775))
- Check sequence type before triggering `unnecessary-enumerate` `len` suggestion ([#7781](https://github.com/astral-sh/ruff/pull/7781))
- Use correct start location for class/function clause header ([#7802](https://github.com/astral-sh/ruff/pull/7802))
- Fix incorrect fixes for `SIM101` ([#7798](https://github.com/astral-sh/ruff/pull/7798))
- Format comment before parameter default correctly ([#7870](https://github.com/astral-sh/ruff/pull/7870))
- Fix `E251` false positive inside f-strings ([#7894](https://github.com/astral-sh/ruff/pull/7894))
- Allow bindings to be created and referenced within annotations ([#7885](https://github.com/astral-sh/ruff/pull/7885))
- Show per-cell diffs when analyzing notebooks over `stdin` ([#7789](https://github.com/astral-sh/ruff/pull/7789))
- Avoid curly brace escape in f-string format spec ([#7780](https://github.com/astral-sh/ruff/pull/7780))
- Fix lexing single-quoted f-string with multi-line format spec ([#7787](https://github.com/astral-sh/ruff/pull/7787))
- Consider nursery rules to be in-preview for `ruff rule` ([#7812](https://github.com/astral-sh/ruff/pull/7812))
- Report precise location for invalid conversion flag ([#7809](https://github.com/astral-sh/ruff/pull/7809))
- Visit pattern match guard as a boolean test ([#7911](https://github.com/astral-sh/ruff/pull/7911))
- Respect `--unfixable` in `ISC` rules ([#7917](https://github.com/astral-sh/ruff/pull/7917))
- Fix edge case with `PIE804` ([#7922](https://github.com/astral-sh/ruff/pull/7922))
- Show custom message in `PTH118` for `Path.joinpath` with starred arguments ([#7852](https://github.com/astral-sh/ruff/pull/7852))
- Fix false negative in `outdated-version-block` when using greater than comparisons ([#7920](https://github.com/astral-sh/ruff/pull/7920))
- Avoid converting f-strings within Django `gettext` calls ([#7898](https://github.com/astral-sh/ruff/pull/7898))
- Fix false positive in `PLR6301` ([#7933](https://github.com/astral-sh/ruff/pull/7933))
- Treat type aliases as typing-only expressions e.g. resolves false positive in `TCH004` ([#7968](https://github.com/astral-sh/ruff/pull/7968))
- Resolve `cache-dir` relative to project root ([#7962](https://github.com/astral-sh/ruff/pull/7962))
- Respect subscripted base classes in type-checking rules e.g. resolves false positive in `TCH003` ([#7954](https://github.com/astral-sh/ruff/pull/7954))
- Fix JSON schema limit for `line-length` ([#7883](https://github.com/astral-sh/ruff/pull/7883))
- Fix commented-out `coalesce` keyword ([#7876](https://github.com/astral-sh/ruff/pull/7876))
### Documentation
- Document `reimplemented-starmap` performance effects ([#7846](https://github.com/astral-sh/ruff/pull/7846))
- Default to following the system dark/light mode ([#7888](https://github.com/astral-sh/ruff/pull/7888))
- Add documentation for fixes ([#7901](https://github.com/astral-sh/ruff/pull/7901))
- Fix typo in docs of `PLR6301` ([#7831](https://github.com/astral-sh/ruff/pull/7831))
- Update `UP038` docs to note that it results in slower code ([#7872](https://github.com/astral-sh/ruff/pull/7872))
- crlf -> cr-lf ([#7766](https://github.com/astral-sh/ruff/pull/7766))
- Add an example of an unsafe fix ([#7924](https://github.com/astral-sh/ruff/pull/7924))
- Fix documented examples for `unnecessary-subscript-reversal` ([#7774](https://github.com/astral-sh/ruff/pull/7774))
- Correct error in tuple example in ruff formatter docs ([#7822](https://github.com/astral-sh/ruff/pull/7822))
- Add versioning policy to documentation ([#7923](https://github.com/astral-sh/ruff/pull/7923))
- Fix invalid code in `FURB177` example ([#7832](https://github.com/astral-sh/ruff/pull/7832))
### Formatter
- Less scary `ruff format` message ([#7867](https://github.com/astral-sh/ruff/pull/7867))
- Remove spaces from import statements ([#7859](https://github.com/astral-sh/ruff/pull/7859))
- Formatter quoting for f-strings with triple quotes ([#7826](https://github.com/astral-sh/ruff/pull/7826))
- Update `ruff_python_formatter` generate.py comment ([#7850](https://github.com/astral-sh/ruff/pull/7850))
- Document one-call chaining deviation ([#7767](https://github.com/astral-sh/ruff/pull/7767))
- Allow f-string modifications in line-shrinking cases ([#7818](https://github.com/astral-sh/ruff/pull/7818))
- Add trailing comment deviation to README ([#7827](https://github.com/astral-sh/ruff/pull/7827))
- Add trailing zero between dot and exponential ([#7956](https://github.com/astral-sh/ruff/pull/7956))
- Force parentheses for power operations in unary expressions ([#7955](https://github.com/astral-sh/ruff/pull/7955))
### Playground
- Fix playground `Quick Fix` action ([#7824](https://github.com/astral-sh/ruff/pull/7824))

View File

@@ -170,7 +170,8 @@ At a high level, the steps involved in adding a new lint rule are as follows:
statements, like imports) or `analyze/expression.rs` (if your rule is based on analyzing
expressions, like function calls).
1. Map the violation struct to a rule code in `crates/ruff_linter/src/codes.rs` (e.g., `B011`).
1. Map the violation struct to a rule code in `crates/ruff_linter/src/codes.rs` (e.g., `B011`). New rules
should be added in `RuleGroup::Preview`.
1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule.

139
Cargo.lock generated
View File

@@ -28,9 +28,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
@@ -74,9 +74,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.5.0"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -112,9 +112,9 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "2.1.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
@@ -221,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"regex-automata 0.3.8",
"regex-automata 0.3.9",
"serde",
]
@@ -313,9 +313,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.5"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
dependencies = [
"clap_builder",
"clap_derive",
@@ -323,9 +323,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.5"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
dependencies = [
"anstream",
"anstyle",
@@ -383,7 +383,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -608,7 +608,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -619,7 +619,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.292"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
@@ -1084,9 +1084,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.33.0"
version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa511b2e298cd49b1856746f6bb73e17036bcd66b25f5e92cdcdbec9bd75686"
checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
dependencies = [
"console",
"globset",
@@ -1129,7 +1129,7 @@ dependencies = [
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -1268,8 +1268,9 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=03179b55ebe7e916f1722e18e8f0b87c01616d1f#03179b55ebe7e916f1722e18e8f0b87c01616d1f"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd5c2ff400caac657bf794181d885491bb97cc37c376f8cb4fa3a0cc2176a053"
dependencies = [
"chic",
"libcst_derive",
@@ -1282,8 +1283,9 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=03179b55ebe7e916f1722e18e8f0b87c01616d1f#03179b55ebe7e916f1722e18e8f0b87c01616d1f"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7f252282b20bfec6fae65d351ab1df7e4552a6270dd7bb779ca9d6135aabe9"
dependencies = [
"quote",
"syn 1.0.109",
@@ -1705,7 +1707,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -1790,9 +1792,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.67"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
@@ -1923,14 +1925,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.5"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
"regex-automata 0.4.3",
"regex-syntax 0.8.2",
]
[[package]]
@@ -1944,13 +1946,19 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
"regex-syntax 0.8.2",
]
[[package]]
@@ -1965,6 +1973,12 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "result-like"
version = "0.4.6"
@@ -2037,7 +2051,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.292"
version = "0.1.0"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2141,6 +2155,7 @@ name = "ruff_diagnostics"
version = "0.0.0"
dependencies = [
"anyhow",
"is-macro",
"log",
"ruff_text_size",
"serde",
@@ -2173,7 +2188,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.0.292"
version = "0.1.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.1",
@@ -2244,7 +2259,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2494,6 +2509,7 @@ dependencies = [
"glob",
"globset",
"ignore",
"is-macro",
"itertools 0.11.0",
"log",
"once_cell",
@@ -2642,9 +2658,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
@@ -2674,7 +2690,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2736,7 +2752,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2765,9 +2781,9 @@ checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "similar"
version = "2.2.1"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597"
[[package]]
name = "siphasher"
@@ -2831,7 +2847,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2847,9 +2863,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.37"
version = "2.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
dependencies = [
"proc-macro2",
"quote",
@@ -2936,7 +2952,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -2948,7 +2964,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
"test-case-core",
]
@@ -2969,7 +2985,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3088,11 +3104,10 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.37"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
@@ -3101,20 +3116,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
name = "tracing-core"
version = "0.1.31"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
@@ -3244,9 +3259,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unicode_names2"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b2c0942619ae1797f999a0ce7efc6c09592ad30e68e16cdbfdcd48a98c3579"
checksum = "5d5506ae2c3c1ccbdf468e52fc5ef536c2ccd981f01273a4cb81aa61021f3a5f"
dependencies = [
"phf",
"unicode_names2_generator",
@@ -3254,9 +3269,9 @@ dependencies = [
[[package]]
name = "unicode_names2_generator"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d0d66ab60be9799a70f8eb227ea43da7dcc47561dd9102cbadacfe0930113f7"
checksum = "b6dfc680313e95bc6637fa278cd7a22390c3c2cd7b8b2bd28755bc6c0fc811e7"
dependencies = [
"getopts",
"log",
@@ -3325,7 +3340,7 @@ checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
]
[[package]]
@@ -3419,7 +3434,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
"wasm-bindgen-shared",
]
@@ -3453,7 +3468,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.38",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@@ -15,47 +15,46 @@ license = "MIT"
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.5", features = ["derive"] }
clap = { version = "4.4.6", features = ["derive"] }
colored = { version = "2.0.0" }
filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.33.0", feature = ["filters", "glob"] }
insta = { version = "1.34.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.0" }
itertools = { version = "0.11.0" }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
memchr = { version = "2.6.4" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.67" }
proc-macro2 = { version = "1.0.69" }
quote = { version = "1.0.23" }
regex = { version = "1.9.5" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
similar = { version = "2.3.0", features = ["inline"] }
smallvec = { version = "1.11.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }
syn = { version = "2.0.37" }
syn = { version = "2.0.38" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.49" }
toml = { version = "0.7.8" }
tracing = { version = "0.1.37" }
tracing = { version = "0.1.39" }
tracing-indicatif = { version = "0.3.4" }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-ident = { version = "1.0.12" }
unicode_names2 = { version = "1.1.0" }
unicode_names2 = { version = "1.2.0" }
unicode-width = { version = "0.1.11" }
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "03179b55ebe7e916f1722e18e8f0b87c01616d1f", default-features = false }
[profile.release]
lto = "fat"
codegen-units = 1

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.292
rev: v0.1.0
hooks:
- id: ruff
```
@@ -172,8 +172,8 @@ If left unspecified, the default configuration is equivalent to:
```toml
[tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
@@ -239,7 +239,7 @@ Rust as a first-party feature.
By default, Ruff enables Flake8's `E` and `F` rules. Ruff supports all rules from the `F` category,
and a [subset](https://docs.astral.sh/ruff/rules/#error-e) of the `E` category, omitting those
stylistic rules made obsolete by the use of an autoformatter, like
stylistic rules made obsolete by the use of a formatter, like
[Black](https://github.com/psf/black).
If you're just getting started with Ruff, **the default rule set is a great place to start**: it

View File

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

View File

@@ -13,6 +13,7 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::rules::flake8_tidy_imports::settings::Strictness;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::settings::types::PythonVersion;
use ruff_linter::settings::DEFAULT_SELECTORS;
use ruff_linter::warn_user;
use ruff_workspace::options::{
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
@@ -25,11 +26,6 @@ use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
const DEFAULT_SELECTORS: &[RuleSelector] = &[
RuleSelector::Linter(Linter::Pyflakes),
RuleSelector::Linter(Linter::Pycodestyle),
];
pub(crate) fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
external_config: &ExternalConfig,

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.292"
version = "0.1.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -9,6 +9,7 @@ use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_workspace::configuration::{Configuration, RuleSelection};
@@ -76,12 +77,18 @@ pub enum Command {
pub struct CheckCommand {
/// List of files or directories to check.
pub files: Vec<PathBuf>,
/// Attempt to automatically fix lint violations.
/// Use `--no-fix` to disable.
/// Apply fixes to resolve lint violations.
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix"))]
fix: bool,
#[clap(long, overrides_with("fix"), hide = true)]
no_fix: bool,
/// Include fixes that may not retain the original intent of the code.
/// Use `--no-unsafe-fixes` to disable.
#[arg(long, overrides_with("no_unsafe_fixes"))]
unsafe_fixes: bool,
#[arg(long, overrides_with("unsafe_fixes"), hide = true)]
no_unsafe_fixes: bool,
/// Show violations with source code.
/// Use `--no-show-source` to disable.
#[arg(long, overrides_with("no_show_source"))]
@@ -100,8 +107,8 @@ pub struct CheckCommand {
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable.
/// Apply fixes to resolve lint violations, but don't report on leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix_only"))]
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
@@ -110,16 +117,6 @@ pub struct CheckCommand {
#[arg(long)]
ignore_noqa: bool,
/// Output serialization format for violations. (Deprecated: Use `--output-format` instead).
#[arg(
long,
value_enum,
env = "RUFF_FORMAT",
conflicts_with = "output_format",
hide = true
)]
pub format: Option<SerializationFormat>,
/// Output serialization format for violations.
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
pub output_format: Option<SerializationFormat>,
@@ -497,8 +494,10 @@ impl CheckCommand {
cache_dir: self.cache_dir,
fix: resolve_bool_arg(self.fix, self.no_fix),
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
.map(UnsafeFixes::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
output_format: self.output_format.or(self.format),
output_format: self.output_format,
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
},
)
@@ -599,6 +598,7 @@ pub struct CliOverrides {
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
pub unsafe_fixes: Option<UnsafeFixes>,
pub force_exclude: Option<bool>,
pub output_format: Option<SerializationFormat>,
pub show_fixes: Option<bool>,
@@ -624,6 +624,9 @@ impl ConfigurationTransformer for CliOverrides {
if let Some(fix_only) = &self.fix_only {
config.fix_only = Some(*fix_only);
}
if self.unsafe_fixes.is_some() {
config.unsafe_fixes = self.unsafe_fixes;
}
config.lint.rule_selections.push(RuleSelection {
select: self.select.clone(),
ignore: self

View File

@@ -338,6 +338,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
#[cfg(test)]
mod tests {
use filetime::{set_file_mtime, FileTime};
use ruff_linter::settings::types::UnsafeFixes;
use std::env::temp_dir;
use std::fs;
use std::io;
@@ -410,6 +411,7 @@ mod tests {
Some(&cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics
@@ -455,6 +457,7 @@ mod tests {
Some(&cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
}
@@ -712,6 +715,7 @@ mod tests {
Some(cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
}
}

View File

@@ -11,6 +11,7 @@ use itertools::Itertools;
use log::{debug, error, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -36,6 +37,7 @@ pub(crate) fn check(
cache: flags::Cache,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
// Collect all the Python files to check.
let start = Instant::now();
@@ -119,7 +121,16 @@ pub(crate) fn check(
}
});
lint_path(path, package, &settings.linter, cache, noqa, fix_mode).map_err(|e| {
lint_path(
path,
package,
&settings.linter,
cache,
noqa,
fix_mode,
unsafe_fixes,
)
.map_err(|e| {
(Some(path.to_owned()), {
let mut error = e.to_string();
for cause in e.chain() {
@@ -199,9 +210,10 @@ fn lint_path(
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
let result = catch_unwind(|| {
crate::diagnostics::lint_path(path, package, settings, cache, noqa, fix_mode)
crate::diagnostics::lint_path(path, package, settings, cache, noqa, fix_mode, unsafe_fixes)
});
match result {
@@ -233,6 +245,8 @@ mod test {
use std::os::unix::fs::OpenOptionsExt;
use anyhow::Result;
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use tempfile::TempDir;
@@ -285,6 +299,7 @@ mod test {
flags::Cache::Disabled,
flags::Noqa::Disabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
let mut output = Vec::new();

View File

@@ -11,6 +11,7 @@ use anyhow::{Context, Result};
use colored::Colorize;
use filetime::FileTime;
use log::{debug, error, warn};
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -168,6 +169,7 @@ pub(crate) fn lint_path(
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
@@ -244,8 +246,15 @@ pub(crate) fn lint_path(
result,
transformed,
fixed,
}) = lint_fix(path, package, noqa, settings, &source_kind, source_type)
{
}) = lint_fix(
path,
package,
noqa,
unsafe_fixes,
settings,
&source_kind,
source_type,
) {
if !fixed.is_empty() {
match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
@@ -355,6 +364,7 @@ pub(crate) fn lint_stdin(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,

View File

@@ -6,11 +6,12 @@ use std::sync::mpsc::channel;
use anyhow::Result;
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff_linter::settings::flags;
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
@@ -104,8 +105,6 @@ pub fn run(
}: Args,
) -> Result<ExitStatus> {
{
use colored::Colorize;
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
#[allow(clippy::print_stderr)]
@@ -166,10 +165,7 @@ pub fn run(
}
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
warn_user_once!(
"`ruff format` is a work-in-progress, subject to change at any time, and intended only for \
experimentation."
);
warn_user_once!("`ruff format` is not yet stable, and subject to change in future versions.");
let (cli, overrides) = args.partition();
@@ -181,14 +177,6 @@ fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
}
pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
if args.format.is_some() {
if std::env::var("RUFF_FORMAT").is_ok() {
warn_user!("The environment variable `RUFF_FORMAT` is deprecated. Use `RUFF_OUTPUT_FORMAT` instead.");
} else {
warn_user!("The argument `--format=<FORMAT>` is deprecated. Use `--output-format=<FORMAT>` instead.");
}
}
let (cli, overrides) = args.partition();
// Construct the "default" settings. These are used when no `pyproject.toml`
@@ -208,6 +196,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
}
_ => Box::new(BufWriter::new(io::stdout())),
};
let stderr_writer = Box::new(BufWriter::new(io::stderr()));
if cli.show_settings {
commands::show_settings::show_settings(
@@ -228,6 +217,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
let Settings {
fix,
fix_only,
unsafe_fixes,
output_format,
show_fixes,
show_source,
@@ -236,17 +226,20 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
// Fix rules are as follows:
// - By default, generate all fixes, but don't apply them to the filesystem.
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
// - If `--fix` or `--fix-only` is set, apply applicable fixes to the filesystem (or
// print them to stdout, if we're reading from stdin).
// - If `--diff` or `--fix-only` are set, don't print any violations (only
// fixes).
// - If `--diff` or `--fix-only` are set, don't print any violations (only applicable fixes)
// - By default, applicable fixes only include [`Applicablility::Automatic`], but if
// `--unsafe-fixes` is set, then [`Applicablility::Suggested`] fixes are included.
let fix_mode = if cli.diff {
flags::FixMode::Diff
FixMode::Diff
} else if fix || fix_only {
flags::FixMode::Apply
FixMode::Apply
} else {
flags::FixMode::Generate
FixMode::Generate
};
let cache = !cli.no_cache;
let noqa = !cli.ignore_noqa;
let mut printer_flags = PrinterFlags::empty();
@@ -290,11 +283,17 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
return Ok(ExitStatus::Success);
}
let printer = Printer::new(output_format, log_level, fix_mode, printer_flags);
let printer = Printer::new(
output_format,
log_level,
fix_mode,
unsafe_fixes,
printer_flags,
);
if cli.watch {
if output_format != SerializationFormat::Text {
warn_user!("--format 'text' is used in watch mode.");
warn_user!("`--output-format text` is always used in watch mode.");
}
// Configure the file watcher.
@@ -318,6 +317,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages)?;
@@ -350,6 +350,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages)?;
}
@@ -376,18 +377,22 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff)) {
if cli.statistics {
printer.write_statistics(&diagnostics, &mut writer)?;
} else {
printer.write_once(&diagnostics, &mut writer)?;
}
// Always try to print violations (though the printer itself may suppress output)
// If we're writing fixes via stdin, the transformed source code goes to the writer
// so send the summary to stderr instead
let mut summary_writer = if is_stdin && matches!(fix_mode, FixMode::Apply | FixMode::Diff) {
stderr_writer
} else {
writer
};
if cli.statistics {
printer.write_statistics(&diagnostics, &mut summary_writer)?;
} else {
printer.write_once(&diagnostics, &mut summary_writer)?;
}
if !cli.exit_zero {

View File

@@ -19,8 +19,8 @@ use ruff_linter::message::{
};
use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule};
use ruff_linter::settings::flags;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
use crate::diagnostics::Diagnostics;
@@ -73,6 +73,7 @@ pub(crate) struct Printer {
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
flags: Flags,
}
@@ -81,12 +82,14 @@ impl Printer {
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
flags: Flags,
) -> Self {
Self {
format,
log_level,
fix_mode,
unsafe_fixes,
flags,
}
}
@@ -99,12 +102,15 @@ impl Printer {
fn write_summary_text(&self, writer: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> {
if self.log_level >= LogLevel::Default {
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
let fixed = diagnostics
.fixed
.values()
.flat_map(std::collections::HashMap::values)
.sum::<usize>();
if self.flags.intersects(Flags::SHOW_VIOLATIONS) {
let fixed = diagnostics
.fixed
.values()
.flat_map(std::collections::HashMap::values)
.sum::<usize>();
let remaining = diagnostics.messages.len();
let total = fixed + remaining;
if fixed > 0 {
@@ -118,32 +124,83 @@ impl Printer {
writeln!(writer, "Found {remaining} error{s}.")?;
}
if show_fix_status(self.fix_mode) {
let num_fixable = diagnostics
.messages
.iter()
.filter(|message| message.fix.is_some())
.count();
if num_fixable > 0 {
writeln!(
writer,
"[{}] {num_fixable} potentially fixable with the --fix option.",
"*".cyan(),
)?;
if let Some(fixables) = fixables {
let fix_prefix = format!("[{}]", "*".cyan());
if self.unsafe_fixes.is_enabled() {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
} else {
if fixables.applicable > 0 && fixables.unapplicable_unsafe > 0 {
let es = if fixables.unapplicable_unsafe == 1 {
""
} else {
"es"
};
writeln!(writer,
"{fix_prefix} {} fixable with the `--fix` option ({} hidden fix{es} can be enabled with the `--unsafe-fixes` option).",
fixables.applicable, fixables.unapplicable_unsafe
)?;
} else if fixables.applicable > 0 {
// Only applicable fixes
writeln!(
writer,
"{fix_prefix} {} fixable with the `--fix` option.",
fixables.applicable,
)?;
} else {
// Only unapplicable fixes
let es = if fixables.unapplicable_unsafe == 1 {
""
} else {
"es"
};
writeln!(writer,
"No fixes available ({} hidden fix{es} can be enabled with the `--unsafe-fixes` option).",
fixables.unapplicable_unsafe
)?;
}
}
}
} else {
let fixed = diagnostics
.fixed
.values()
.flat_map(std::collections::HashMap::values)
.sum::<usize>();
if fixed > 0 {
let s = if fixed == 1 { "" } else { "s" };
if self.fix_mode.is_apply() {
writeln!(writer, "Fixed {fixed} error{s}.")?;
// Check if there are unapplied fixes
let unapplied = {
if let Some(fixables) = fixables {
fixables.unapplicable_unsafe
} else {
writeln!(writer, "Would fix {fixed} error{s}.")?;
0
}
};
if unapplied > 0 {
let es = if unapplied == 1 { "" } else { "es" };
if fixed > 0 {
let s = if fixed == 1 { "" } else { "s" };
if self.fix_mode.is_apply() {
writeln!(writer, "Fixed {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`).")?;
} else {
writeln!(writer, "Would fix {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`).")?;
}
} else {
if self.fix_mode.is_apply() {
writeln!(writer, "No errors fixed ({unapplied} fix{es} available with `--unsafe-fixes`).")?;
} else {
writeln!(writer, "No errors would be fixed ({unapplied} fix{es} available with `--unsafe-fixes`).")?;
}
}
} else {
if fixed > 0 {
let s = if fixed == 1 { "" } else { "s" };
if self.fix_mode.is_apply() {
writeln!(writer, "Fixed {fixed} error{s}.")?;
} else {
writeln!(writer, "Would fix {fixed} error{s}.")?;
}
}
}
}
@@ -178,6 +235,7 @@ impl Printer {
}
let context = EmitterContext::new(&diagnostics.notebook_indexes);
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
match self.format {
SerializationFormat::Json => {
@@ -191,9 +249,10 @@ impl Printer {
}
SerializationFormat::Text => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
@@ -209,7 +268,8 @@ impl Printer {
SerializationFormat::Grouped => {
GroupedEmitter::default()
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
@@ -359,6 +419,8 @@ impl Printer {
);
}
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
if !diagnostics.messages.is_empty() {
if self.log_level >= LogLevel::Default {
writeln!(writer)?;
@@ -366,8 +428,9 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
}
writer.flush()?;
@@ -390,13 +453,13 @@ fn num_digits(n: usize) -> usize {
}
/// Return `true` if the [`Printer`] should indicate that a rule is fixable.
const fn show_fix_status(fix_mode: flags::FixMode) -> bool {
fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics>) -> bool {
// If we're in application mode, avoid indicating that a rule is fixable.
// If the specific violation were truly fixable, it would've been fixed in
// this pass! (We're occasionally unable to determine whether a specific
// violation is fixable without trying to fix it, so if fix is not
// enabled, we may inadvertently indicate that a rule is fixable.)
!fix_mode.is_apply()
(!fix_mode.is_apply()) && fixables.is_some_and(FixableStatistics::any_applicable_fixes)
}
fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>) -> Result<()> {
@@ -439,3 +502,43 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>
}
Ok(())
}
/// Statistics for [applicable][ruff_diagnostics::Applicability] fixes.
#[derive(Debug)]
struct FixableStatistics {
applicable: u32,
unapplicable_unsafe: u32,
}
impl FixableStatistics {
fn try_from(diagnostics: &Diagnostics, unsafe_fixes: UnsafeFixes) -> Option<Self> {
let mut applicable = 0;
let mut unapplicable_unsafe = 0;
for message in &diagnostics.messages {
if let Some(fix) = &message.fix {
if fix.applies(unsafe_fixes.required_applicability()) {
applicable += 1;
} else {
// Do not include unapplicable fixes at other levels that do not provide an opt-in
if fix.applicability().is_unsafe() {
unapplicable_unsafe += 1;
}
}
}
}
if applicable == 0 && unapplicable_unsafe == 0 {
None
} else {
Some(Self {
applicable,
unapplicable_unsafe,
})
}
}
fn any_applicable_fixes(&self) -> bool {
self.applicable > 0
}
}

View File

@@ -39,7 +39,7 @@ if condition:
print('Hy "Micha"') # Should not change quotes
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
}
@@ -83,7 +83,7 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
@@ -139,12 +139,12 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
/// Tests that the legacy `format` option continues to work but emits a warning.
/// Since 0.1.0 the legacy format option is no longer supported
#[test]
fn legacy_format_option() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -156,54 +156,29 @@ format = "json"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "F401",
"end_location": {
"column": 10,
"row": 2
},
"filename": "-",
"fix": {
"applicability": {
"automatic": "safe"
},
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 3
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`os` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
}
]
----- stderr -----
warning: The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.
"###);
insta::with_settings!({filters => vec![
(&*regex::escape(ruff_toml.to_str().unwrap()), "[RUFF-TOML-PATH]"),
]}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Failed to parse `[RUFF-TOML-PATH]`: TOML parse error at line 2, column 10
|
2 | format = "json"
| ^^^^^^
invalid type: string "json", expected struct FormatOptions
"###);
});
Ok(())
}

View File

@@ -1,6 +1,5 @@
#![cfg(not(target_family = "wasm"))]
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::fs::Permissions;
@@ -12,13 +11,13 @@ use std::process::Command;
use std::str;
#[cfg(unix)]
use anyhow::{Context, Result};
use anyhow::Context;
use anyhow::Result;
#[cfg(unix)]
use clap::Parser;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
#[cfg(unix)]
use path_absolutize::path_dedot;
#[cfg(unix)]
use tempfile::TempDir;
#[cfg(unix)]
@@ -46,16 +45,16 @@ fn stdin_success() {
fn stdin_error() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.pass_stdin("import os\n"), @r#"
.pass_stdin("import os\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"#);
"###);
}
#[test]
@@ -69,7 +68,7 @@ fn stdin_filename() {
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -87,7 +86,7 @@ fn stdin_source_type_py() {
----- stdout -----
TCH.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -150,6 +149,7 @@ fn stdin_fix_py() {
print(sys.version)
----- stderr -----
Found 1 error (1 fixed, 0 remaining).
"###);
}
@@ -317,6 +317,7 @@ fn stdin_fix_jupyter() {
"nbformat_minor": 5
}
----- stderr -----
Found 2 errors (2 fixed, 0 remaining).
"###);
}
@@ -336,6 +337,8 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
print(sys.version)
----- stderr -----
-:3:4: F634 If test is a tuple, which is always `True`
Found 2 errors (1 fixed, 1 remaining).
"###);
}
@@ -482,7 +485,7 @@ fn stdin_format_jupyter() {
}
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
}
@@ -733,6 +736,42 @@ fn preview_disabled_prefix_empty() {
"###);
}
#[test]
fn preview_disabled_does_not_warn_for_empty_ignore_selections() {
// Does not warn that the selection is empty since the user is not trying to enable the rule
let args = ["--ignore", "CPY"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn preview_disabled_does_not_warn_for_empty_fixable_selections() {
// Does not warn that the selection is empty since the user is not trying to enable the rule
let args = ["--fixable", "CPY"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn preview_group_selector() {
// `--select PREVIEW` should error (selector was removed)
@@ -861,7 +900,7 @@ fn check_input_from_argfile() -> Result<()> {
----- stdout -----
/path/to/a.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -869,3 +908,499 @@ fn check_input_from_argfile() -> Result<()> {
Ok(())
}
#[test]
fn check_hints_hidden_unsafe_fixes() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format=text",
"--isolated",
"--select",
"F601,UP034",
"--no-cache",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
}
#[test]
fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["-", "--output-format", "text", "--no-cache", "--isolated", "--select", "F601"])
.pass_stdin("x = {'a': 1, 'a': 1}\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
}
#[test]
fn check_shows_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format=text",
"--isolated",
"--select",
"F601,UP034",
"--no-cache",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 [*] Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 2 fixable with the --fix option.
----- stderr -----
"###);
}
#[test]
fn fix_applies_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
x = {'a': 1, 'a': 1}
print('foo')
----- stderr -----
-:1:14: F601 Dictionary key literal `'a'` repeated
Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
}
#[test]
fn fix_applies_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1}
print('foo')
----- stderr -----
Found 2 errors (2 fixed, 0 remaining).
"###);
}
#[test]
fn fix_does_not_apply_display_only_fixes() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"B006",
"--fix",
])
.pass_stdin("def add_to_list(item, some_list=[]): ..."),
@r###"
success: false
exit_code: 1
----- stdout -----
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:33: B006 Do not use mutable data structures for argument defaults
Found 1 error.
"###);
}
#[test]
fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"B006",
"--fix",
"--unsafe-fixes",
])
.pass_stdin("def add_to_list(item, some_list=[]): ..."),
@r###"
success: false
exit_code: 1
----- stdout -----
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:33: B006 Do not use mutable data structures for argument defaults
Found 1 error.
"###);
}
#[test]
fn fix_only_unsafe_fixes_available() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601",
"--fix",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
x = {'a': 1, 'a': 1}
print(('foo'))
----- stderr -----
-:1:14: F601 Dictionary key literal `'a'` repeated
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
}
#[test]
fn fix_only_flag_applies_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix-only",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1, 'a': 1}
print('foo')
----- stderr -----
Fixed 1 error (1 additional fix available with `--unsafe-fixes`).
"###);
}
#[test]
fn fix_only_flag_applies_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix-only",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1}
print('foo')
----- stderr -----
Fixed 2 errors.
"###);
}
#[test]
fn diff_shows_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--diff",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -1,2 +1,2 @@
x = {'a': 1, 'a': 1}
-print(('foo'))
+print('foo')
----- stderr -----
Would fix 1 error (1 additional fix available with `--unsafe-fixes`).
"###
);
}
#[test]
fn diff_shows_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--diff",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -1,2 +1,2 @@
-x = {'a': 1, 'a': 1}
-print(('foo'))
+x = {'a': 1}
+print('foo')
----- stderr -----
Would fix 2 errors.
"###
);
}
#[test]
fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"B006",
"--diff",
"--unsafe-fixes",
])
.pass_stdin("def add_to_list(item, some_list=[]): ..."),
@r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
"###);
}
#[test]
fn diff_only_unsafe_fixes_available() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601",
"--diff",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
No errors would be fixed (1 fix available with `--unsafe-fixes`).
"###
);
}
#[test]
fn check_extend_unsafe_fixes() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
extend-unsafe-fixes = ["UP034"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--config"])
.arg(&ruff_toml)
.arg("-")
.args([
"--output-format",
"text",
"--no-cache",
"--select",
"F601,UP034",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
-:2:7: UP034 Avoid extraneous parentheses
Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
Ok(())
}
#[test]
fn check_extend_safe_fixes() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
extend-safe-fixes = ["F601"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--config"])
.arg(&ruff_toml)
.arg("-")
.args([
"--output-format",
"text",
"--no-cache",
"--select",
"F601,UP034",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 [*] Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn check_extend_unsafe_fixes_conflict_with_extend_safe_fixes() -> Result<()> {
// Adding a rule to both options should result in it being treated as unsafe
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
extend-unsafe-fixes = ["UP034"]
extend-safe-fixes = ["UP034"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--config"])
.arg(&ruff_toml)
.arg("-")
.args([
"--output-format",
"text",
"--no-cache",
"--select",
"F601,UP034",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
-:2:7: UP034 Avoid extraneous parentheses
Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
Ok(())
}

View File

@@ -40,7 +40,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -75,7 +75,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -110,7 +110,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@@ -149,7 +149,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);

View File

@@ -17,6 +17,7 @@ exit_code: 1
----- stdout -----
[
{
"cell": null,
"code": "F401",
"end_location": {
"column": 10,
@@ -24,9 +25,7 @@ exit_code: 1
},
"filename": "/path/to/F401.py",
"fix": {
"applicability": {
"automatic": "safe"
},
"applicability": "safe",
"edits": [
{
"content": "",

View File

@@ -17,4 +17,5 @@ ruff_text_size = { path = "../ruff_text_size" }
anyhow = { workspace = true }
log = { workspace = true }
is-macro = { workspace = true }
serde = { workspace = true, optional = true, features = [] }

View File

@@ -6,30 +6,21 @@ use ruff_text_size::{Ranged, TextSize};
use crate::edit::Edit;
/// Indicates if a fix can be applied.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, is_macro::Is)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Applicability {
/// The fix can be applied programmatically.
/// The fix is likely to be correct and the resulting code will have valid syntax.
Automatic(Safety),
/// The fix should only be manually applied by the user.
/// The fix is unsafe and should only be displayed for manual application by the user.
/// The fix is likely to be incorrect or the resulting code may have invalid syntax.
Manual,
}
Display,
/// Indicates the safety of applying a fix.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[serde(rename_all = "lowercase")]
pub enum Safety {
/// The fix is definitely what the user intended, or it maintains the exact meaning of the code.
/// This fix can be automatically applied.
Safe,
/// The fix may be what the user intended, but it is uncertain.
/// The fix can be applied with user opt-in.
/// The fix is unsafe and should only be applied with user opt-in.
/// The fix may be what the user intended, but it is uncertain; the resulting code will have valid syntax.
Unsafe,
/// The fix is safe and can always be applied.
/// The fix is definitely what the user intended, or it maintains the exact meaning of the code.
Safe,
}
/// Indicates the level of isolation required to apply a fix.
@@ -56,62 +47,62 @@ pub struct Fix {
}
impl Fix {
/// Create a new [`Fix`] with [safe applicability](Applicability::Automatic(Safety::Safe)) from an [`Edit`] element.
pub fn automatic_safe(edit: Edit) -> Self {
/// Create a new [`Fix`] that is [safe](Applicability::Safe) to apply from an [`Edit`] element.
pub fn safe_edit(edit: Edit) -> Self {
Self {
edits: vec![edit],
applicability: Applicability::Automatic(Safety::Safe),
applicability: Applicability::Safe,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with [safe applicability](Applicability::Automatic(Safety::Safe)) from multiple [`Edit`] elements.
pub fn automatic_safe_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
/// Create a new [`Fix`] that is [safe](Applicability::Safe) to apply from multiple [`Edit`] elements.
pub fn safe_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
Self {
edits,
applicability: Applicability::Automatic(Safety::Safe),
applicability: Applicability::Safe,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with [unsafe applicability](Applicable::Automatic(Safety::Unsafe)) from an [`Edit`] element.
pub fn automatic_unsafe(edit: Edit) -> Self {
/// Create a new [`Fix`] that is [unsafe](Applicability::Unsafe) to apply from an [`Edit`] element.
pub fn unsafe_edit(edit: Edit) -> Self {
Self {
edits: vec![edit],
applicability: Applicability::Automatic(Safety::Unsafe),
applicability: Applicability::Unsafe,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with [unsafe applicability](Applicability::Automatic(Safety::Unsafe)) from multiple [`Edit`] elements.
pub fn automatic_unsafe_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
/// Create a new [`Fix`] that is [unsafe](Applicability::Unsafe) to apply from multiple [`Edit`] elements.
pub fn unsafe_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
Self {
edits,
applicability: Applicability::Automatic(Safety::Unsafe),
applicability: Applicability::Unsafe,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with [manual applicability](Applicability::Manual) from an [`Edit`] element.
pub fn manual(edit: Edit) -> Self {
/// Create a new [`Fix`] that should only [display](Applicability::Display) and not apply from an [`Edit`] element .
pub fn display_edit(edit: Edit) -> Self {
Self {
edits: vec![edit],
applicability: Applicability::Manual,
applicability: Applicability::Display,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with [manual applicability](Applicability::Manual) from multiple [`Edit`] elements.
pub fn manual_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
/// Create a new [`Fix`] that should only [display](Applicability::Display) and not apply from multiple [`Edit`] elements.
pub fn display_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
Self {
edits,
applicability: Applicability::Manual,
applicability: Applicability::Display,
isolation_level: IsolationLevel::default(),
}
}
@@ -142,4 +133,16 @@ impl Fix {
self.isolation_level = isolation;
self
}
/// Return [`true`] if this [`Fix`] should be applied with at a given [`Applicability`].
pub fn applies(&self, applicability: Applicability) -> bool {
self.applicability >= applicability
}
/// Create a new [`Fix`] with the given [`Applicability`].
#[must_use]
pub fn with_applicability(mut self, applicability: Applicability) -> Self {
self.applicability = applicability;
self
}
}

View File

@@ -1,6 +1,6 @@
pub use diagnostic::{Diagnostic, DiagnosticKind};
pub use edit::Edit;
pub use fix::{Applicability, Fix, IsolationLevel, Safety};
pub use fix::{Applicability, Fix, IsolationLevel};
pub use source_map::{SourceMap, SourceMarker};
pub use violation::{AlwaysFixableViolation, FixAvailability, Violation};

View File

@@ -20,7 +20,7 @@ rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
static_assertions = { workspace = true }
tracing = { version = "0.1.37", default-features = false, features = ["std"] }
tracing = { workspace = true }
unicode-width = { workspace = true }
[dev-dependencies]

View File

@@ -54,7 +54,7 @@ impl<'a> Printer<'a> {
/// Prints the passed in element as well as all its content,
/// starting at the specified indentation level
#[tracing::instrument(name = "Printer::print", skip_all)]
#[tracing::instrument(level = "debug", name = "Printer::print", skip_all)]
pub fn print_with_indent(
mut self,
document: &'a Document,

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.0.292"
version = "0.1.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -29,7 +29,7 @@ ruff_python_parser = { path = "../ruff_python_parser" }
ruff_source_file = { path = "../ruff_source_file", features = ["serde"] }
ruff_text_size = { path = "../ruff_text_size" }
aho-corasick = { version = "1.1.1" }
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
bitflags = { workspace = true }
@@ -59,7 +59,7 @@ regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { version = "1.0.19" }
semver = { version = "1.0.20" }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }

View File

@@ -70,6 +70,8 @@ settings.set_enable_developer_extras(True)
foo.is_(True)
bar.is_not(False)
next(iter([]), False)
sa.func.coalesce(tbl.c.valid, False)
class Registry:
def __init__(self) -> None:

View File

@@ -8,6 +8,8 @@ Foo.objects.create(**{"_id": some_id}) # PIE804
Foo.objects.create(**{**bar}) # PIE804
foo(**{})
foo(**{**data, "foo": "buzz"})
foo(**buzz)

View File

@@ -28,4 +28,12 @@ item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
x: type[requests_mock.Mocker] | type[httpretty] | type[str] = requests_mock.Mocker
y: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
def func():
from typing import Union as U
# PYI055
x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker

View File

@@ -21,4 +21,5 @@ item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
item: type[requests_mock.Mocker] | type[httpretty] | type[str] = requests_mock.Mocker
item2: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker

View File

@@ -42,3 +42,7 @@ with contextlib.ExitStack():
with contextlib.ExitStack() as exit_stack:
exit_stack_ = exit_stack
f = exit_stack_.enter_context(open("filename"))
# OK (quick one-liner to clear file contents)
open("filename", "w").close()
pathlib.Path("filename").open("w").close()

View File

@@ -0,0 +1,19 @@
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .foo import Record
type RecordOrThings = Record | int | str
type RecordCallback[R: Record] = Callable[[R], None]
def process_record[R: Record](record: R) -> None:
...
class RecordContainer[R: Record]:
def add_record(self, record: R) -> None:
...

View File

@@ -22,3 +22,10 @@ class C:
class D(C):
x: UUID
import collections
class E(BaseModel[int]):
x: collections.Awaitable

View File

@@ -33,6 +33,8 @@ with open(p) as fp:
fp.read()
open(p).close()
os.getcwdb(p)
os.path.join(p, *q)
os.sep.join(p, *q)
# https://github.com/astral-sh/ruff/issues/7620
def opener(path, flags):

View File

@@ -54,3 +54,8 @@ f"{a=}"
f"{a:=1}"
f"{foo(a=1)}"
f"normal {f"{a=}"} normal"
# Okay as the `=` is used inside a f-string...
print(f"{foo = }")
# ...but then it creates false negatives for now
print(f"{foo(a = 1)}")

View File

@@ -0,0 +1,10 @@
def with_backslash():
"""Sum\\mary."""
def ends_in_quote():
'Sum\\mary."'
def contains_quote():
'Sum"\\mary.'

View File

@@ -0,0 +1,19 @@
"""Test bindings created within annotations."""
from typing import Annotated
foo = [1, 2, 3, 4, 5]
class Bar:
# OK: Allow list comprehensions in annotations (i.e., treat `qux` as a valid
# load in the scope of the annotation).
baz: Annotated[
str,
[qux for qux in foo],
]
# OK: Allow named expressions in annotations.
x: (y := 1)
print(y)

View File

@@ -0,0 +1,21 @@
"""Test bindings created within annotations under `__future__` annotations."""
from __future__ import annotations
from typing import Annotated
foo = [1, 2, 3, 4, 5]
class Bar:
# OK: Allow list comprehensions in annotations (i.e., treat `qux` as a valid
# load in the scope of the annotation).
baz: Annotated[
str,
[qux for qux in foo],
]
# Error: `y` is not defined.
x: (y := 1)
print(y)

View File

@@ -0,0 +1,73 @@
# OK
1<2 and 'b' and 'c'
1<2 or 'a' and 'b'
1<2 and 'a'
1<2 or 'a'
2>1
1<2 and 'a' or 'b' and 'c'
1<2 and 'a' or 'b' or 'c'
1<2 and 'a' or 'b' or 'c' or (lambda x: x+1)
1<2 and 'a' or 'b' or (lambda x: x+1) or 'c'
default = 'default'
if (not isinstance(default, bool) and isinstance(default, int)) \
or (isinstance(default, str) and default):
pass
docid, token = None, None
(docid is None and token is None) or (docid is not None and token is not None)
vendor, os_version = 'darwin', '14'
vendor == "debian" and os_version in ["12"] or vendor == "ubuntu" and os_version in []
# Don't emit if the parent is an `if` statement.
if (task_id in task_dict and task_dict[task_id] is not task) \
or task_id in used_group_ids:
pass
no_target, is_x64, target = True, False, 'target'
if (no_target and not is_x64) or target == 'ARM_APPL_RUST_TARGET':
pass
# Don't emit if the parent is a `bool_op` expression.
isinstance(val, str) and ((len(val) == 7 and val[0] == "#") or val in enums.NamedColor)
# Errors
1<2 and 'a' or 'b'
(lambda x: x+1) and 'a' or 'b'
'a' and (lambda x: x+1) or 'orange'
val = '#0000FF'
(len(val) == 7 and val[0] == "#") or val in {'green'}
marker = 'marker'
isinstance(marker, dict) and 'field' in marker or marker in {}
def has_oranges(oranges, apples=None) -> bool:
return apples and False or oranges
[x for x in l if a and b or c]
{x: y for x in l if a and b or c}
{x for x in l if a and b or c}
new_list = [
x
for sublist in all_lists
if a and b or c
for x in sublist
if (isinstance(operator, list) and x in operator) or x != operator
]

View File

@@ -63,6 +63,15 @@ class Apples:
def __html__(self):
pass
# Allow _missing_, used by enum.Enum.
@classmethod
def _missing_(cls, value):
pass
# Allow anonymous functions.
def _(self):
pass
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -1,8 +1,19 @@
# Errors
for item in {1}:
print(f"I can count to {item}!")
for item in {"apples", "lemons", "water"}: # flags in-line set literals
print(f"I like {item}.")
for item in {1,}:
print(f"I can count to {item}!")
for item in {
"apples", "lemons", "water"
}: # flags in-line set literals
print(f"I like {item}.")
numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions

View File

@@ -0,0 +1,10 @@
# Errors
1 in [1, 2, 3]
1 in (1, 2, 3)
1 in (
1, 2, 3
)
# OK
fruits = ["cherry", "grapes"]
"cherry" in fruits

View File

@@ -0,0 +1,71 @@
# bare raise is an except block
try:
pass
except Exception:
raise
try:
pass
except Exception:
if True:
raise
# bare raise is in a finally block which is in an except block
try:
raise Exception
except Exception:
try:
raise Exception
finally:
raise
# bare raise is in an __exit__ method
class ContextManager:
def __enter__(self):
return self
def __exit__(self, *args):
raise
try:
raise # [misplaced-bare-raise]
except Exception:
pass
def f():
try:
raise # [misplaced-bare-raise]
except Exception:
pass
def g():
raise # [misplaced-bare-raise]
def h():
try:
if True:
def i():
raise # [misplaced-bare-raise]
except Exception:
pass
raise # [misplaced-bare-raise]
raise # [misplaced-bare-raise]
try:
pass
except:
def i():
raise # [misplaced-bare-raise]
try:
pass
except:
class C:
raise # [misplaced-bare-raise]
try:
pass
except:
pass
finally:
raise # [misplaced-bare-raise]

View File

@@ -1,5 +1,7 @@
import abc
from typing_extensions import override
class Person:
def developer_greeting(self, name): # [no-self-use]
@@ -60,3 +62,24 @@ class Prop:
@property
def count(self):
return 24
class A:
def foo(self):
...
class B(A):
@override
def foo(self):
...
def foobar(self):
super()
def bar(self):
super().foo()
def baz(self):
if super().foo():
...

View File

@@ -0,0 +1,54 @@
if a:
...
elif (a and b):
...
elif (a and b) and c:
...
elif (a and b) and c and d:
...
elif (a and b) and c and d and e:
...
elif (a and b) and c and d and e and f:
...
elif (a and b) and c and d and e and f and g:
...
elif (a and b) and c and d and e and f and g and h:
...
elif (a and b) and c and d and e and f and g and h and i:
...
elif (a and b) and c and d and e and f and g and h and i and j:
...
elif (a and b) and c and d and e and f and g and h and i and j and k:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y and z:
...
else:
...

View File

@@ -0,0 +1,44 @@
import io
import sys
import tempfile
import io as hugo
import codecs
# Errors.
open("test.txt")
io.TextIOWrapper(io.FileIO("test.txt"))
hugo.TextIOWrapper(hugo.FileIO("test.txt"))
tempfile.NamedTemporaryFile("w")
tempfile.TemporaryFile("w")
codecs.open("test.txt")
tempfile.SpooledTemporaryFile(0, "w")
# Non-errors.
open("test.txt", encoding="utf-8")
open("test.bin", "wb")
open("test.bin", mode="wb")
open("test.txt", "r", -1, "utf-8")
open("test.txt", mode=sys.argv[1])
def func(*args, **kwargs):
open(*args)
open("text.txt", **kwargs)
io.TextIOWrapper(io.FileIO("test.txt"), encoding="utf-8")
io.TextIOWrapper(io.FileIO("test.txt"), "utf-8")
tempfile.TemporaryFile("w", encoding="utf-8")
tempfile.TemporaryFile("w", -1, "utf-8")
tempfile.TemporaryFile("wb")
tempfile.TemporaryFile()
tempfile.NamedTemporaryFile("w", encoding="utf-8")
tempfile.NamedTemporaryFile("w", -1, "utf-8")
tempfile.NamedTemporaryFile("wb")
tempfile.NamedTemporaryFile()
codecs.open("test.txt", encoding="utf-8")
codecs.open("test.bin", "wb")
codecs.open("test.bin", mode="wb")
codecs.open("test.txt", "r", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "w", encoding="utf-8")
tempfile.SpooledTemporaryFile(0, "w", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "wb")
tempfile.SpooledTemporaryFile(0, )

View File

@@ -202,3 +202,8 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"{}".format(
1 # comment
)
# The fixed string will exceed the line length, but it's still smaller than the
# existing line length, so it's fine.
"<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)

View File

@@ -0,0 +1,9 @@
from django.utils.translation import gettext
long = 'long'
split_to = 'split_to'
gettext(
'some super {} and complicated string so that the error code '
'E501 Triggers when this is not {} multi-line'.format(
long, split_to)
)

View File

@@ -196,3 +196,9 @@ if sys.version_info < (3,10000000):
if sys.version_info <= (3,10000000):
print("py3")
if sys.version_info > (3,12):
print("py3")
if sys.version_info >= (3,12):
print("py3")

View File

@@ -0,0 +1,7 @@
import typing
from typing import TypeAlias
# UP040
# Fixes in type stub files should be safe to apply unlike in regular code where runtime behavior could change
x: typing.TypeAlias = int
x: TypeAlias = int

View File

@@ -0,0 +1,45 @@
# Errors.
if 1 in (1,):
print("Single-element tuple")
if 1 in [1]:
print("Single-element list")
if 1 in {1}:
print("Single-element set")
if "a" in "a":
print("Single-element string")
if 1 not in (1,):
print("Check `not in` membership test")
if not 1 in (1,):
print("Check the negated membership test")
# Non-errors.
if 1 in (1, 2):
pass
if 1 in [1, 2]:
pass
if 1 in {1, 2}:
pass
if "a" in "ab":
pass
if 1 == (1,):
pass
if 1 > [1]:
pass
if 1 is {1}:
pass
if "a" == "a":
pass

View File

@@ -38,6 +38,13 @@ y = range(10)
list(range(10))[0]
list(x.y)[0]
list(x["y"])[0]
[*range(10)][0]
[*x["y"]][0]
[*x.y][0]
[* x.y][0]
[
*x.y
][0]
# RUF015 (multi-line)
revision_heads_map_ast = [
@@ -45,3 +52,12 @@ revision_heads_map_ast = [
for a in revision_heads_map_ast_obj.body
if isinstance(a, ast.Assign) and a.targets[0].id == "REVISION_HEADS_MAP"
][0]
# RUF015 (zip)
list(zip(x, y))[0]
[*zip(x, y)][0]
def test():
zip = list # Overwrite the builtin zip
list(zip(x, y))[0]

View File

@@ -0,0 +1,7 @@
# RUF018
assert (x := 0) == 0
assert x, (y := "error")
# OK
if z := 0:
pass

View File

@@ -0,0 +1,20 @@
d = {}
# RUF019
if "k" in d and d["k"]:
pass
k = "k"
if k in d and d[k]:
pass
if (k) in d and d[k]:
pass
if k in d and d[(k)]:
pass
# OK
v = "k" in d and d["k"]
if f() in d and d[f()]:
pass

View File

@@ -32,15 +32,10 @@ pub(crate) fn bindings(checker: &mut Checker) {
},
binding.range(),
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {
pyflakes::fixes::remove_exception_handler_assignment(
binding,
checker.locator,
)
.map(Fix::automatic_safe)
});
}
diagnostic.try_set_fix(|| {
pyflakes::fixes::remove_exception_handler_assignment(binding, checker.locator)
.map(Fix::safe_edit)
});
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -306,7 +306,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
pylint::rules::no_self_use(checker, scope_id, scope, &mut diagnostics);
}
}
}

View File

@@ -414,13 +414,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::format_literals(checker, call, &summary);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
checker,
call,
&summary,
value,
checker.settings.line_length,
);
pyupgrade::rules::f_strings(checker, call, &summary, value);
}
}
}
@@ -792,6 +786,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SubprocessRunWithoutCheck) {
pylint::rules::subprocess_run_without_check(checker, call);
}
if checker.enabled(Rule::UnspecifiedEncoding) {
pylint::rules::unspecified_encoding(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
@@ -1200,6 +1197,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::ComparisonWithItself) {
pylint::rules::comparison_with_itself(checker, left, ops, comparators);
}
if checker.enabled(Rule::LiteralMembership) {
pylint::rules::literal_membership(checker, compare);
}
if checker.enabled(Rule::ComparisonOfConstant) {
pylint::rules::comparison_of_constant(checker, left, ops, comparators);
}
@@ -1224,6 +1224,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
comparators,
);
}
if checker.enabled(Rule::SingleItemMembershipTest) {
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
@@ -1415,6 +1418,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::RepeatedEqualityComparison) {
pylint::rules::repeated_equality_comparison(checker, bool_op);
}
if checker.enabled(Rule::AndOrTernary) {
pylint::rules::and_or_ternary(checker, bool_op);
}
if checker.enabled(Rule::UnnecessaryKeyCheck) {
ruff::rules::unnecessary_key_check(checker, expr);
}
}
Expr::NamedExpr(..) => {
if checker.enabled(Rule::AssignmentInAssert) {
ruff::rules::assignment_in_assert(checker, expr);
}
}
_ => {}
};

View File

@@ -964,7 +964,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::Raise(ast::StmtRaise { exc, .. }) => {
Stmt::Raise(raise @ ast::StmtRaise { exc, .. }) => {
if checker.enabled(Rule::RaiseNotImplemented) {
if let Some(expr) = exc {
pyflakes::rules::raise_not_implemented(checker, expr);
@@ -1004,6 +1004,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_raise::rules::unnecessary_paren_on_raise_exception(checker, expr);
}
}
if checker.enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => {
if checker.enabled(Rule::GlobalStatement) {
@@ -1067,6 +1070,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
}
if checker.enabled(Rule::TooManyBooleanExpressions) {
pylint::rules::too_many_boolean_expressions(checker, if_);
}
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,

View File

@@ -143,11 +143,6 @@ impl<'a> Checker<'a> {
}
impl<'a> Checker<'a> {
/// Return `true` if a patch should be generated for a given [`Rule`].
pub(crate) fn patch(&self, code: Rule) -> bool {
self.settings.rules.should_fix(code)
}
/// Return `true` if a [`Rule`] is disabled by a `noqa` directive.
pub(crate) fn rule_is_ignored(&self, code: Rule, offset: TextSize) -> bool {
// TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`.
@@ -529,6 +524,7 @@ where
);
self.semantic.push_definition(definition);
self.semantic.push_scope(ScopeKind::Function(function_def));
self.semantic.flags -= SemanticModelFlags::EXCEPTION_HANDLER;
self.deferred.functions.push(self.semantic.snapshot());
@@ -567,6 +563,7 @@ where
);
self.semantic.push_definition(definition);
self.semantic.push_scope(ScopeKind::Class(class_def));
self.semantic.flags -= SemanticModelFlags::EXCEPTION_HANDLER;
// Extract any global bindings from the class body.
if let Some(globals) = Globals::from_body(body) {
@@ -585,7 +582,9 @@ where
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
self.visit_expr(value);
// The value in a `type` alias has annotation semantics, in that it's never
// evaluated at runtime.
self.visit_annotation(value);
self.semantic.pop_scope();
self.visit_expr(name);
}
@@ -1168,13 +1167,14 @@ where
range: _,
}) = slice.as_ref()
{
if let Some(expr) = elts.first() {
let mut iter = elts.iter();
if let Some(expr) = iter.next() {
self.visit_expr(expr);
for expr in elts.iter().skip(1) {
self.visit_non_type_definition(expr);
}
self.visit_expr_context(ctx);
}
for expr in iter {
self.visit_non_type_definition(expr);
}
self.visit_expr_context(ctx);
} else {
debug!("Found non-Expr::Tuple argument to PEP 593 Annotation.");
}
@@ -1366,7 +1366,7 @@ where
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
self.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
self.visit_boolean_test(expr);
}
self.semantic.push_branch();
@@ -1618,10 +1618,12 @@ impl<'a> Checker<'a> {
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.current_statement();
// Match the left-hand side of an annotated assignment, like `x` in `x: int`.
if matches!(
parent,
Stmt::AnnAssign(ast::StmtAnnAssign { value: None, .. })
) {
) && !self.semantic.in_annotation()
{
self.add_binding(
id,
expr.range(),
@@ -1768,7 +1770,7 @@ impl<'a> Checker<'a> {
bound: Some(bound), ..
}) = type_param
{
self.visit_expr(bound);
self.visit_annotation(bound);
}
}
}

View File

@@ -5,7 +5,7 @@ use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::registry::{AsRule, Rule};
use crate::registry::AsRule;
use crate::rules::pycodestyle::rules::logical_lines::{
extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword,
missing_whitespace_around_operator, space_after_comma, space_around_operator,
@@ -38,17 +38,6 @@ pub(crate) fn check_logical_lines(
) -> Vec<Diagnostic> {
let mut context = LogicalLinesContext::new(settings);
let should_fix_missing_whitespace = settings.rules.should_fix(Rule::MissingWhitespace);
let should_fix_whitespace_before_parameters =
settings.rules.should_fix(Rule::WhitespaceBeforeParameters);
let should_fix_whitespace_after_open_bracket =
settings.rules.should_fix(Rule::WhitespaceAfterOpenBracket);
let should_fix_whitespace_before_close_bracket = settings
.rules
.should_fix(Rule::WhitespaceBeforeCloseBracket);
let should_fix_whitespace_before_punctuation =
settings.rules.should_fix(Rule::WhitespaceBeforePunctuation);
let mut prev_line = None;
let mut prev_indent_level = None;
let indent_char = stylist.indentation().as_char();
@@ -58,7 +47,7 @@ pub(crate) fn check_logical_lines(
space_around_operator(&line, &mut context);
whitespace_around_named_parameter_equals(&line, &mut context);
missing_whitespace_around_operator(&line, &mut context);
missing_whitespace(&line, should_fix_missing_whitespace, &mut context);
missing_whitespace(&line, &mut context);
}
if line.flags().contains(TokenFlags::PUNCTUATION) {
space_after_comma(&line, &mut context);
@@ -68,13 +57,7 @@ pub(crate) fn check_logical_lines(
.flags()
.intersects(TokenFlags::OPERATOR | TokenFlags::BRACKET | TokenFlags::PUNCTUATION)
{
extraneous_whitespace(
&line,
&mut context,
should_fix_whitespace_after_open_bracket,
should_fix_whitespace_before_close_bracket,
should_fix_whitespace_before_punctuation,
);
extraneous_whitespace(&line, &mut context);
}
if line.flags().contains(TokenFlags::KEYWORD) {
@@ -87,11 +70,7 @@ pub(crate) fn check_logical_lines(
}
if line.flags().contains(TokenFlags::BRACKET) {
whitespace_before_parameters(
&line,
should_fix_whitespace_before_parameters,
&mut context,
);
whitespace_before_parameters(&line, &mut context);
}
// Extract the indentation level.

View File

@@ -109,12 +109,8 @@ pub(crate) fn check_noqa(
if line.matches.is_empty() {
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic_unsafe(delete_noqa(
directive.range(),
locator,
)));
}
diagnostic.set_fix(Fix::safe_edit(delete_noqa(directive.range(), locator)));
diagnostics.push(diagnostic);
}
}
@@ -175,18 +171,14 @@ pub(crate) fn check_noqa(
},
directive.range(),
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
diagnostic.set_fix(Fix::automatic_unsafe(delete_noqa(
directive.range(),
locator,
)));
} else {
diagnostic.set_fix(Fix::automatic_unsafe(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));
}
if valid_codes.is_empty() {
diagnostic
.set_fix(Fix::safe_edit(delete_noqa(directive.range(), locator)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));
}
diagnostics.push(diagnostic);
}

View File

@@ -71,11 +71,7 @@ pub(crate) fn check_physical_lines(
}
if enforce_no_newline_at_end_of_file {
if let Some(diagnostic) = no_newline_at_end_of_file(
locator,
stylist,
settings.rules.should_fix(Rule::MissingNewlineAtEndOfFile),
) {
if let Some(diagnostic) = no_newline_at_end_of_file(locator, stylist) {
diagnostics.push(diagnostic);
}
}

View File

@@ -72,7 +72,7 @@ pub(crate) fn check_tokens(
}
if settings.rules.enabled(Rule::UTF8EncodingDeclaration) {
pyupgrade::rules::unnecessary_coding_comment(&mut diagnostics, locator, indexer, settings);
pyupgrade::rules::unnecessary_coding_comment(&mut diagnostics, locator, indexer);
}
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
@@ -83,7 +83,6 @@ pub(crate) fn check_tokens(
indexer,
tok,
*range,
settings.rules.should_fix(Rule::InvalidEscapeSequence),
);
}
}
@@ -109,13 +108,7 @@ pub(crate) fn check_tokens(
Rule::MultipleStatementsOnOneLineSemicolon,
Rule::UselessSemicolon,
]) {
pycodestyle::rules::compound_statements(
&mut diagnostics,
tokens,
locator,
indexer,
settings,
);
pycodestyle::rules::compound_statements(&mut diagnostics, tokens, locator, indexer);
}
if settings.rules.enabled(Rule::AvoidableEscapedQuote) && settings.flake8_quotes.avoid_escape {
@@ -137,7 +130,7 @@ pub(crate) fn check_tokens(
flake8_implicit_str_concat::rules::implicit(
&mut diagnostics,
tokens,
&settings.flake8_implicit_str_concat,
settings,
locator,
indexer,
);
@@ -148,11 +141,11 @@ pub(crate) fn check_tokens(
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
]) {
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, settings);
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator);
}
if settings.rules.enabled(Rule::ExtraneousParentheses) {
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator, settings);
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator);
}
if is_stub && settings.rules.enabled(Rule::TypeCommentInStub) {
@@ -166,7 +159,7 @@ pub(crate) fn check_tokens(
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]) {
flake8_executable::rules::from_tokens(tokens, path, locator, settings, &mut diagnostics);
flake8_executable::rules::from_tokens(tokens, path, locator, &mut diagnostics);
}
if settings.rules.any_enabled(&[
@@ -191,7 +184,7 @@ pub(crate) fn check_tokens(
TodoComment::from_comment(comment, *comment_range, i)
})
.collect();
flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer, settings);
flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer);
flake8_fixme::rules::todos(&mut diagnostics, &todo_comments);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,16 +3,18 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::fix::codemods;
use crate::line_width::{LineLength, LineWidthBuilder, TabSize};
/// Return the `Fix` to use when deleting a `Stmt`.
///
@@ -285,6 +287,75 @@ pub(crate) fn pad(content: String, range: TextRange, locator: &Locator) -> Strin
)
}
/// Returns `true` if the fix fits within the maximum configured line length.
pub(crate) fn fits(
fix: &str,
node: AnyNodeRef,
locator: &Locator,
line_length: LineLength,
tab_size: TabSize,
) -> bool {
all_lines_fit(fix, node, locator, line_length.value() as usize, tab_size)
}
/// Returns `true` if the fix fits within the maximum configured line length, or produces lines that
/// are shorter than the maximum length of the existing AST node.
pub(crate) fn fits_or_shrinks(
fix: &str,
node: AnyNodeRef,
locator: &Locator,
line_length: LineLength,
tab_size: TabSize,
) -> bool {
// Use the larger of the line length limit, or the longest line in the existing AST node.
let line_length = std::iter::once(line_length.value() as usize)
.chain(
locator
.slice(locator.lines_range(node.range()))
.universal_newlines()
.map(|line| LineWidthBuilder::new(tab_size).add_str(&line).get()),
)
.max()
.unwrap_or(line_length.value() as usize);
all_lines_fit(fix, node, locator, line_length, tab_size)
}
/// Returns `true` if all lines in the fix are shorter than the given line length.
fn all_lines_fit(
fix: &str,
node: AnyNodeRef,
locator: &Locator,
line_length: usize,
tab_size: TabSize,
) -> bool {
let prefix = locator.slice(TextRange::new(
locator.line_start(node.start()),
node.start(),
));
// Ensure that all lines are shorter than the line length limit.
fix.universal_newlines().enumerate().all(|(idx, line)| {
// If `template` is a multiline string, `col_offset` should only be applied to the first
// line:
// ```
// a = """{} -> offset = col_offset (= 4)
// {} -> offset = 0
// """.format(0, 1) -> offset = 0
// ```
let measured_length = if idx == 0 {
LineWidthBuilder::new(tab_size)
.add_str(prefix)
.add_str(&line)
.get()
} else {
LineWidthBuilder::new(tab_size).add_str(&line).get()
};
measured_length <= line_length
})
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -9,6 +9,7 @@ use ruff_source_file::Locator;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
use crate::settings::types::UnsafeFixes;
pub(crate) mod codemods;
pub(crate) mod edits;
@@ -23,11 +24,22 @@ pub(crate) struct FixResult {
pub(crate) source_map: SourceMap,
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<FixResult> {
/// Fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(
diagnostics: &[Diagnostic],
locator: &Locator,
unsafe_fixes: UnsafeFixes,
) -> Option<FixResult> {
let required_applicability = unsafe_fixes.required_applicability();
let mut with_fixes = diagnostics
.iter()
.filter(|diag| diag.fix.is_some())
.filter(|diagnostic| {
diagnostic
.fix
.as_ref()
.map_or(false, |fix| fix.applies(required_applicability))
})
.peekable();
if with_fixes.peek().is_none() {
@@ -151,7 +163,7 @@ mod tests {
// The choice of rule here is arbitrary.
kind: MissingNewlineAtEndOfFile.into(),
range: edit.range(),
fix: Some(Fix::automatic_safe(edit)),
fix: Some(Fix::safe_edit(edit)),
parent: None,
})
.collect()

View File

@@ -1,19 +1,23 @@
use ruff_cache::{CacheKey, CacheKeyHasher};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::hash::Hasher;
use std::num::{NonZeroU16, NonZeroU8, ParseIntError};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthChar;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_macros::CacheKey;
use ruff_text_size::TextSize;
/// The length of a line of text that is considered too long.
///
/// The allowed range of values is 1..=320
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LineLength(NonZeroU16);
pub struct LineLength(
#[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] NonZeroU16,
);
impl LineLength {
/// Maximum allowed value for a valid [`LineLength`]
@@ -23,6 +27,10 @@ impl LineLength {
pub fn value(&self) -> u16 {
self.0.get()
}
pub fn text_len(&self) -> TextSize {
TextSize::from(u32::from(self.value()))
}
}
impl Default for LineLength {

View File

@@ -8,7 +8,7 @@ use itertools::Itertools;
use log::error;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{Applicability, Diagnostic};
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
@@ -32,6 +32,7 @@ use crate::message::Message;
use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle;
use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
@@ -259,6 +260,36 @@ pub fn check_path(
}
}
// Remove fixes for any rules marked as unfixable.
for diagnostic in &mut diagnostics {
if !settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic.fix = None;
}
}
// Update fix applicability to account for overrides
if !settings.extend_safe_fixes.is_empty() || !settings.extend_unsafe_fixes.is_empty() {
for diagnostic in &mut diagnostics {
if let Some(fix) = diagnostic.fix.take() {
// Enforce demotions over promotions so if someone puts a rule in both we are conservative
if fix.applicability().is_safe()
&& settings
.extend_unsafe_fixes
.contains(diagnostic.kind.rule())
{
diagnostic.set_fix(fix.with_applicability(Applicability::Unsafe));
} else if fix.applicability().is_unsafe()
&& settings.extend_safe_fixes.contains(diagnostic.kind.rule())
{
diagnostic.set_fix(fix.with_applicability(Applicability::Safe));
} else {
// Retain the existing fix (will be dropped from `.take()` otherwise)
diagnostic.set_fix(fix);
}
}
}
}
LinterResult::new((diagnostics, imports), error)
}
@@ -415,10 +446,12 @@ fn diagnostics_to_messages(
/// Generate `Diagnostic`s from source code content, iteratively fixing
/// until stable.
#[allow(clippy::too_many_arguments)]
pub fn lint_fix<'a>(
path: &Path,
package: Option<&Path>,
noqa: flags::Noqa,
unsafe_fixes: UnsafeFixes,
settings: &LinterSettings,
source_kind: &'a SourceKind,
source_type: PySourceType,
@@ -494,7 +527,7 @@ pub fn lint_fix<'a>(
code: fixed_contents,
fixes: applied,
source_map,
}) = fix_file(&result.data.0, &locator)
}) = fix_file(&result.data.0, &locator, unsafe_fixes)
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.

View File

@@ -177,18 +177,15 @@ impl Display for DisplayParseError<'_> {
f,
"cell {cell}{colon}",
cell = jupyter_index
.cell(source_location.row.get())
.unwrap_or_default(),
.cell(source_location.row)
.unwrap_or(OneIndexed::MIN),
colon = ":".cyan(),
)?;
SourceLocation {
row: OneIndexed::new(
jupyter_index
.cell_row(source_location.row.get())
.unwrap_or(1) as usize,
)
.unwrap(),
row: jupyter_index
.cell_row(source_location.row)
.unwrap_or(OneIndexed::MIN),
column: source_location.column,
}
} else {

View File

@@ -6,7 +6,7 @@ use colored::{Color, ColoredString, Colorize, Styles};
use ruff_text_size::{Ranged, TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use ruff_diagnostics::{Applicability, Fix, Safety};
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::{OneIndexed, SourceFile};
use crate::message::Message;
@@ -35,6 +35,7 @@ impl<'a> Diff<'a> {
impl Display for Diff<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
let mut output = String::with_capacity(self.source_code.source_text().len());
let mut last_end = TextSize::default();
@@ -53,11 +54,9 @@ impl Display for Diff<'_> {
let message = match self.fix.applicability() {
// TODO(zanieb): Adjust this messaging once it's user-facing
Applicability::Automatic(safety) => match safety {
Safety::Safe => "Fix",
Safety::Unsafe => "Unsafe fix",
},
Applicability::Manual => "Manual fix",
Applicability::Safe => "Fix",
Applicability::Unsafe => "Suggested fix",
Applicability::Display => "Possible fix",
};
writeln!(f, " {}", message.blue())?;

View File

@@ -13,11 +13,13 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::settings::types::UnsafeFixes;
#[derive(Default)]
pub struct GroupedEmitter {
show_fix_status: bool,
show_source: bool,
unsafe_fixes: UnsafeFixes,
}
impl GroupedEmitter {
@@ -32,6 +34,12 @@ impl GroupedEmitter {
self.show_source = show_source;
self
}
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.unsafe_fixes = unsafe_fixes;
self
}
}
impl Emitter for GroupedEmitter {
@@ -68,6 +76,7 @@ impl Emitter for GroupedEmitter {
notebook_index: context.notebook_index(message.filename()),
message,
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes,
show_source: self.show_source,
row_length,
column_length,
@@ -89,6 +98,7 @@ impl Emitter for GroupedEmitter {
struct DisplayGroupedMessage<'a> {
message: MessageWithLocation<'a>,
show_fix_status: bool,
unsafe_fixes: UnsafeFixes,
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
@@ -115,18 +125,18 @@ impl Display for DisplayGroupedMessage<'_> {
f,
"cell {cell}{sep}",
cell = jupyter_index
.cell(start_location.row.get())
.unwrap_or_default(),
.cell(start_location.row)
.unwrap_or(OneIndexed::MIN),
sep = ":".cyan()
)?;
(
jupyter_index
.cell_row(start_location.row.get())
.unwrap_or(1) as usize,
start_location.column.get(),
.cell_row(start_location.row)
.unwrap_or(OneIndexed::MIN),
start_location.column,
)
} else {
(start_location.row.get(), start_location.column.get())
(start_location.row, start_location.column)
};
writeln!(
@@ -138,7 +148,8 @@ impl Display for DisplayGroupedMessage<'_> {
),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.show_fix_status
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes
},
)?;
@@ -196,6 +207,7 @@ mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GroupedEmitter;
use crate::settings::types::UnsafeFixes;
#[test]
fn default() {
@@ -222,4 +234,15 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn fix_status_unsafe() {
let mut emitter = GroupedEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
}
}

View File

@@ -5,7 +5,8 @@ use serde::{Serialize, Serializer};
use serde_json::{json, Value};
use ruff_diagnostics::Edit;
use ruff_source_file::SourceCode;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message};
@@ -19,9 +20,9 @@ impl Emitter for JsonEmitter {
&mut self,
writer: &mut dyn Write,
messages: &[Message],
_context: &EmitterContext,
context: &EmitterContext,
) -> anyhow::Result<()> {
serde_json::to_writer_pretty(writer, &ExpandedMessages { messages })?;
serde_json::to_writer_pretty(writer, &ExpandedMessages { messages, context })?;
Ok(())
}
@@ -29,6 +30,7 @@ impl Emitter for JsonEmitter {
struct ExpandedMessages<'a> {
messages: &'a [Message],
context: &'a EmitterContext<'a>,
}
impl Serialize for ExpandedMessages<'_> {
@@ -39,7 +41,7 @@ impl Serialize for ExpandedMessages<'_> {
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
for message in self.messages {
let value = message_to_json_value(message);
let value = message_to_json_value(message, self.context);
s.serialize_element(&value)?;
}
@@ -47,26 +49,40 @@ impl Serialize for ExpandedMessages<'_> {
}
}
pub(crate) fn message_to_json_value(message: &Message) -> Value {
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
let source_code = message.file.to_source_code();
let notebook_index = context.notebook_index(message.filename());
let fix = message.fix.as_ref().map(|fix| {
json!({
"applicability": fix.applicability(),
"message": message.kind.suggestion.as_deref(),
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code },
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index },
})
});
let start_location = source_code.source_location(message.start());
let end_location = source_code.source_location(message.end());
let noqa_location = source_code.source_location(message.noqa_offset);
let mut start_location = source_code.source_location(message.start());
let mut end_location = source_code.source_location(message.end());
let mut noqa_location = source_code.source_location(message.noqa_offset);
let mut notebook_cell_index = None;
if let Some(notebook_index) = notebook_index {
notebook_cell_index = Some(
notebook_index
.cell(start_location.row)
.unwrap_or(OneIndexed::MIN),
);
start_location = notebook_index.translate_location(&start_location);
end_location = notebook_index.translate_location(&end_location);
noqa_location = notebook_index.translate_location(&noqa_location);
}
json!({
"code": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
"message": message.kind.body,
"fix": fix,
"cell": notebook_cell_index,
"location": start_location,
"end_location": end_location,
"filename": message.filename(),
@@ -77,6 +93,7 @@ pub(crate) fn message_to_json_value(message: &Message) -> Value {
struct ExpandedEdits<'a> {
edits: &'a [Edit],
source_code: &'a SourceCode<'a, 'a>,
notebook_index: Option<&'a NotebookIndex>,
}
impl Serialize for ExpandedEdits<'_> {
@@ -87,10 +104,57 @@ impl Serialize for ExpandedEdits<'_> {
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
for edit in self.edits {
let mut location = self.source_code.source_location(edit.start());
let mut end_location = self.source_code.source_location(edit.end());
if let Some(notebook_index) = self.notebook_index {
// There exists a newline between each cell's source code in the
// concatenated source code in Ruff. This newline doesn't actually
// exists in the JSON source field.
//
// Now, certain edits may try to remove this newline, which means
// the edit will spill over to the first character of the next cell.
// If it does, we need to translate the end location to the last
// character of the previous cell.
match (
notebook_index.cell(location.row),
notebook_index.cell(end_location.row),
) {
(Some(start_cell), Some(end_cell)) if start_cell != end_cell => {
debug_assert_eq!(end_location.column.get(), 1);
let prev_row = end_location.row.saturating_sub(1);
end_location = SourceLocation {
row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
column: self
.source_code
.source_location(self.source_code.line_end_exclusive(prev_row))
.column,
};
}
(Some(_), None) => {
debug_assert_eq!(end_location.column.get(), 1);
let prev_row = end_location.row.saturating_sub(1);
end_location = SourceLocation {
row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
column: self
.source_code
.source_location(self.source_code.line_end_exclusive(prev_row))
.column,
};
}
_ => {
end_location = notebook_index.translate_location(&end_location);
}
}
location = notebook_index.translate_location(&location);
}
let value = json!({
"content": edit.content().unwrap_or_default(),
"location": self.source_code.source_location(edit.start()),
"end_location": self.source_code.source_location(edit.end())
"location": location,
"end_location": end_location
});
s.serialize_element(&value)?;
@@ -104,7 +168,10 @@ impl Serialize for ExpandedEdits<'_> {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
};
use crate::message::JsonEmitter;
#[test]
@@ -114,4 +181,13 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonEmitter;
let (messages, notebook_indexes) = create_notebook_messages();
let content = capture_emitter_notebook_output(&mut emitter, &messages, &notebook_indexes);
assert_snapshot!(content);
}
}

View File

@@ -11,11 +11,11 @@ impl Emitter for JsonLinesEmitter {
&mut self,
writer: &mut dyn Write,
messages: &[Message],
_context: &EmitterContext,
context: &EmitterContext,
) -> anyhow::Result<()> {
let mut w = writer;
for message in messages {
serde_json::to_writer(&mut w, &message_to_json_value(message))?;
serde_json::to_writer(&mut w, &message_to_json_value(message, context))?;
w.write_all(b"\n")?;
}
Ok(())
@@ -27,7 +27,10 @@ mod tests {
use insta::assert_snapshot;
use crate::message::json_lines::JsonLinesEmitter;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
};
#[test]
fn output() {
@@ -36,4 +39,13 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonLinesEmitter;
let (messages, notebook_indexes) = create_notebook_messages();
let content = capture_emitter_notebook_output(&mut emitter, &messages, &notebook_indexes);
assert_snapshot!(content);
}
}

View File

@@ -150,7 +150,8 @@ mod tests {
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_source_file::SourceFileBuilder;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{OneIndexed, SourceFileBuilder};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};
@@ -178,7 +179,7 @@ def fibonacci(n):
},
TextRange::new(TextSize::from(7), TextSize::from(9)),
)
.with_fix(Fix::automatic_unsafe(Edit::range_deletion(TextRange::new(
.with_fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(0),
TextSize::from(10),
))));
@@ -193,7 +194,7 @@ def fibonacci(n):
},
TextRange::new(TextSize::from(94), TextSize::from(95)),
)
.with_fix(Fix::automatic_unsafe(Edit::deletion(
.with_fix(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94),
TextSize::from(99),
)));
@@ -221,6 +222,113 @@ def fibonacci(n):
]
}
pub(super) fn create_notebook_messages() -> (Vec<Message>, FxHashMap<String, NotebookIndex>) {
let notebook = r"# cell 1
import os
# cell 2
import math
print('hello world')
# cell 3
def foo():
print()
x = 1
";
let unused_import_os = Diagnostic::new(
DiagnosticKind {
name: "UnusedImport".to_string(),
body: "`os` imported but unused".to_string(),
suggestion: Some("Remove unused import: `os`".to_string()),
},
TextRange::new(TextSize::from(16), TextSize::from(18)),
)
.with_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(9),
TextSize::from(19),
))));
let unused_import_math = Diagnostic::new(
DiagnosticKind {
name: "UnusedImport".to_string(),
body: "`math` imported but unused".to_string(),
suggestion: Some("Remove unused import: `math`".to_string()),
},
TextRange::new(TextSize::from(35), TextSize::from(39)),
)
.with_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(28),
TextSize::from(40),
))));
let unused_variable = Diagnostic::new(
DiagnosticKind {
name: "UnusedVariable".to_string(),
body: "Local variable `x` is assigned to but never used".to_string(),
suggestion: Some("Remove assignment to unused variable `x`".to_string()),
},
TextRange::new(TextSize::from(98), TextSize::from(99)),
)
.with_fix(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94),
TextSize::from(104),
)));
let notebook_source = SourceFileBuilder::new("notebook.ipynb", notebook).finish();
let mut notebook_indexes = FxHashMap::default();
notebook_indexes.insert(
"notebook.ipynb".to_string(),
NotebookIndex::new(
vec![
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
],
vec![
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(3),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(3),
],
),
);
let unused_import_os_start = unused_import_os.start();
let unused_import_math_start = unused_import_math.start();
let unused_variable_start = unused_variable.start();
(
vec![
Message::from_diagnostic(
unused_import_os,
notebook_source.clone(),
unused_import_os_start,
),
Message::from_diagnostic(
unused_import_math,
notebook_source.clone(),
unused_import_math_start,
),
Message::from_diagnostic(unused_variable, notebook_source, unused_variable_start),
],
notebook_indexes,
)
}
pub(super) fn capture_emitter_output(
emitter: &mut dyn Emitter,
messages: &[Message],
@@ -232,4 +340,16 @@ def fibonacci(n):
String::from_utf8(output).expect("Output to be valid UTF-8")
}
pub(super) fn capture_emitter_notebook_output(
emitter: &mut dyn Emitter,
messages: &[Message],
notebook_indexes: &FxHashMap<String, NotebookIndex>,
) -> String {
let context = EmitterContext::new(notebook_indexes);
let mut output: Vec<u8> = Vec::new();
emitter.emit(&mut output, messages, &context).unwrap();
String::from_utf8(output).expect("Output to be valid UTF-8")
}
}

View File

@@ -3,14 +3,14 @@ source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
fib.py:
1:8 F401 [*] `os` imported but unused
1:8 F401 `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
6:5 F841 [*] Local variable `x` is assigned to but never used
6:5 F841 Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""

View File

@@ -0,0 +1,31 @@
---
source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
fib.py:
1:8 F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
6:5 F841 [*] Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^ F841
7 | if n == 0:
8 | return 0
|
= help: Remove assignment to unused variable `x`
undef.py:
1:4 F821 Undefined name `a`
|
1 | if a == 1: pass
| ^ F821
|

View File

@@ -0,0 +1,105 @@
---
source: crates/ruff_linter/src/message/json.rs
expression: content
---
[
{
"cell": 1,
"code": "F401",
"end_location": {
"column": 10,
"row": 2
},
"filename": "notebook.ipynb",
"fix": {
"applicability": "safe",
"edits": [
{
"content": "",
"end_location": {
"column": 10,
"row": 2
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`os` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": 2,
"code": "F401",
"end_location": {
"column": 12,
"row": 2
},
"filename": "notebook.ipynb",
"fix": {
"applicability": "safe",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 3
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `math`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`math` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": 3,
"code": "F841",
"end_location": {
"column": 6,
"row": 4
},
"filename": "notebook.ipynb",
"fix": {
"applicability": "unsafe",
"edits": [
{
"content": "",
"end_location": {
"column": 10,
"row": 4
},
"location": {
"column": 1,
"row": 4
}
}
],
"message": "Remove assignment to unused variable `x`"
},
"location": {
"column": 5,
"row": 4
},
"message": "Local variable `x` is assigned to but never used",
"noqa_row": 4,
"url": "https://docs.astral.sh/ruff/rules/unused-variable"
}
]

View File

@@ -4,6 +4,7 @@ expression: content
---
[
{
"cell": null,
"code": "F401",
"end_location": {
"column": 10,
@@ -11,9 +12,7 @@ expression: content
},
"filename": "fib.py",
"fix": {
"applicability": {
"automatic": "unsafe"
},
"applicability": "unsafe",
"edits": [
{
"content": "",
@@ -38,6 +37,7 @@ expression: content
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": null,
"code": "F841",
"end_location": {
"column": 6,
@@ -45,9 +45,7 @@ expression: content
},
"filename": "fib.py",
"fix": {
"applicability": {
"automatic": "unsafe"
},
"applicability": "unsafe",
"edits": [
{
"content": "",
@@ -72,6 +70,7 @@ expression: content
"url": "https://docs.astral.sh/ruff/rules/unused-variable"
},
{
"cell": null,
"code": "F821",
"end_location": {
"column": 5,

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/message/json_lines.rs
expression: content
---
{"cell":1,"code":"F401","end_location":{"column":10,"row":2},"filename":"notebook.ipynb","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":10,"row":2},"location":{"column":1,"row":2}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":2},"message":"`os` imported but unused","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":2,"code":"F401","end_location":{"column":12,"row":2},"filename":"notebook.ipynb","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":3},"location":{"column":1,"row":2}}],"message":"Remove unused import: `math`"},"location":{"column":8,"row":2},"message":"`math` imported but unused","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":3,"code":"F841","end_location":{"column":6,"row":4},"filename":"notebook.ipynb","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":10,"row":4},"location":{"column":1,"row":4}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":4},"message":"Local variable `x` is assigned to but never used","noqa_row":4,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/message/json_lines.rs
expression: content
---
{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":{"automatic":"unsafe"},"edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":{"automatic":"unsafe"},"edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":null,"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
{"cell":null,"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}

View File

@@ -2,14 +2,14 @@
source: crates/ruff_linter/src/message/text.rs
expression: content
---
fib.py:1:8: F401 [*] `os` imported but unused
fib.py:1:8: F401 `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""

View File

@@ -0,0 +1,29 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
fib.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^ F841
7 | if n == 0:
8 | return 0
|
= help: Remove assignment to unused variable `x`
undef.py:1:4: F821 Undefined name `a`
|
1 | if a == 1: pass
| ^ F821
|

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
notebook.ipynb:cell 1:2:8: F401 [*] `os` imported but unused
|
1 | # cell 1
2 | import os
| ^^ F401
|
= help: Remove unused import: `os`
notebook.ipynb:cell 2:2:8: F401 [*] `math` imported but unused
|
1 | # cell 2
2 | import math
| ^^^^ F401
3 |
4 | print('hello world')
|
= help: Remove unused import: `math`
notebook.ipynb:cell 3:4:5: F841 [*] Local variable `x` is assigned to but never used
|
2 | def foo():
3 | print()
4 | x = 1
| ^ F841
|
= help: Remove assignment to unused variable `x`

View File

@@ -16,22 +16,24 @@ use crate::line_width::{LineWidthBuilder, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::settings::types::UnsafeFixes;
bitflags! {
#[derive(Default)]
struct EmitterFlags: u8 {
/// Whether to show the fix status of a diagnostic.
const SHOW_FIX_STATUS = 0b0000_0001;
const SHOW_FIX_STATUS = 0b0000_0001;
/// Whether to show the diff of a fix, for diagnostics that have a fix.
const SHOW_FIX_DIFF = 0b0000_0010;
const SHOW_FIX_DIFF = 0b0000_0010;
/// Whether to show the source code of a diagnostic.
const SHOW_SOURCE = 0b0000_0100;
const SHOW_SOURCE = 0b0000_0100;
}
}
#[derive(Default)]
pub struct TextEmitter {
flags: EmitterFlags,
unsafe_fixes: UnsafeFixes,
}
impl TextEmitter {
@@ -53,6 +55,12 @@ impl TextEmitter {
self.flags.set(EmitterFlags::SHOW_SOURCE, show_source);
self
}
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.unsafe_fixes = unsafe_fixes;
self
}
}
impl Emitter for TextEmitter {
@@ -79,18 +87,15 @@ impl Emitter for TextEmitter {
writer,
"cell {cell}{sep}",
cell = notebook_index
.cell(start_location.row.get())
.unwrap_or_default(),
.cell(start_location.row)
.unwrap_or(OneIndexed::MIN),
sep = ":".cyan(),
)?;
SourceLocation {
row: OneIndexed::new(
notebook_index
.cell_row(start_location.row.get())
.unwrap_or(1) as usize,
)
.unwrap(),
row: notebook_index
.cell_row(start_location.row)
.unwrap_or(OneIndexed::MIN),
column: start_location.column,
}
} else {
@@ -105,7 +110,8 @@ impl Emitter for TextEmitter {
sep = ":".cyan(),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS)
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
unsafe_fixes: self.unsafe_fixes,
}
)?;
@@ -134,28 +140,33 @@ impl Emitter for TextEmitter {
pub(super) struct RuleCodeAndBody<'a> {
pub(crate) message: &'a Message,
pub(crate) show_fix_status: bool,
pub(crate) unsafe_fixes: UnsafeFixes,
}
impl Display for RuleCodeAndBody<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let kind = &self.message.kind;
if self.show_fix_status {
if let Some(fix) = self.message.fix.as_ref() {
// Do not display an indicator for unapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) {
return write!(
f,
"{code} {fix}{body}",
code = kind.rule().noqa_code().to_string().red().bold(),
fix = format_args!("[{}] ", "*".cyan()),
body = kind.body,
);
}
}
};
if self.show_fix_status && self.message.fix.is_some() {
write!(
f,
"{code} {fix}{body}",
code = kind.rule().noqa_code().to_string().red().bold(),
fix = format_args!("[{}] ", "*".cyan()),
body = kind.body,
)
} else {
write!(
f,
"{code} {body}",
code = kind.rule().noqa_code().to_string().red().bold(),
body = kind.body,
)
}
write!(
f,
"{code} {body}",
code = kind.rule().noqa_code().to_string().red().bold(),
body = kind.body,
)
}
}
@@ -189,9 +200,9 @@ impl Display for MessageCodeFrame<'_> {
// If we're working with a Jupyter Notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(index) = self.notebook_index {
let content_start_cell = index.cell(content_start_index.get()).unwrap_or_default();
let content_start_cell = index.cell(content_start_index).unwrap_or(OneIndexed::MIN);
while start_index < content_start_index {
if index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
if index.cell(start_index).unwrap_or(OneIndexed::MIN) == content_start_cell {
break;
}
start_index = start_index.saturating_add(1);
@@ -214,9 +225,9 @@ impl Display for MessageCodeFrame<'_> {
// If we're working with a Jupyter Notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(index) = self.notebook_index {
let content_end_cell = index.cell(content_end_index.get()).unwrap_or_default();
let content_end_cell = index.cell(content_end_index).unwrap_or(OneIndexed::MIN);
while end_index > content_end_index {
if index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
if index.cell(end_index).unwrap_or(OneIndexed::MIN) == content_end_cell {
break;
}
end_index = end_index.saturating_sub(1);
@@ -256,8 +267,9 @@ impl Display for MessageCodeFrame<'_> {
|| start_index.get(),
|notebook_index| {
notebook_index
.cell_row(start_index.get())
.unwrap_or_default() as usize
.cell_row(start_index)
.unwrap_or(OneIndexed::MIN)
.get()
},
),
annotations: vec![SourceAnnotation {
@@ -339,8 +351,12 @@ struct SourceCode<'a> {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
};
use crate::message::TextEmitter;
use crate::settings::types::UnsafeFixes;
#[test]
fn default() {
@@ -359,4 +375,27 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn fix_status_unsafe() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
let (messages, notebook_indexes) = create_notebook_messages();
let content = capture_emitter_notebook_output(&mut emitter, &messages, &notebook_indexes);
assert_snapshot!(content);
}
}

View File

@@ -3,7 +3,6 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::registry::Rule;
use crate::settings::LinterSettings;
use super::super::detection::comment_contains_code;
@@ -67,11 +66,9 @@ pub(crate) fn commented_out_code(
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
if settings.rules.should_fix(Rule::CommentedOutCode) {
diagnostic.set_fix(Fix::manual(Edit::range_deletion(
locator.full_lines_range(*range),
)));
}
diagnostic.set_fix(Fix::display_edit(Edit::range_deletion(
locator.full_lines_range(*range),
)));
diagnostics.push(diagnostic);
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/eradicate/mod.rs
---
ERA001.py:1:1: ERA001 [*] Found commented-out code
ERA001.py:1:1: ERA001 Found commented-out code
|
1 | #import os
| ^^^^^^^^^^ ERA001
@@ -10,13 +10,13 @@ ERA001.py:1:1: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
1 |-#import os
2 1 | # from foo import junk
3 2 | #a = 3
4 3 | a = 4
ERA001.py:2:1: ERA001 [*] Found commented-out code
ERA001.py:2:1: ERA001 Found commented-out code
|
1 | #import os
2 | # from foo import junk
@@ -26,14 +26,14 @@ ERA001.py:2:1: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
1 1 | #import os
2 |-# from foo import junk
3 2 | #a = 3
4 3 | a = 4
5 4 | #foo(1, 2, 3)
ERA001.py:3:1: ERA001 [*] Found commented-out code
ERA001.py:3:1: ERA001 Found commented-out code
|
1 | #import os
2 | # from foo import junk
@@ -44,7 +44,7 @@ ERA001.py:3:1: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
1 1 | #import os
2 2 | # from foo import junk
3 |-#a = 3
@@ -52,7 +52,7 @@ ERA001.py:3:1: ERA001 [*] Found commented-out code
5 4 | #foo(1, 2, 3)
6 5 |
ERA001.py:5:1: ERA001 [*] Found commented-out code
ERA001.py:5:1: ERA001 Found commented-out code
|
3 | #a = 3
4 | a = 4
@@ -63,7 +63,7 @@ ERA001.py:5:1: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
2 2 | # from foo import junk
3 3 | #a = 3
4 4 | a = 4
@@ -72,7 +72,7 @@ ERA001.py:5:1: ERA001 [*] Found commented-out code
7 6 | def foo(x, y, z):
8 7 | content = 1 # print('hello')
ERA001.py:13:5: ERA001 [*] Found commented-out code
ERA001.py:13:5: ERA001 Found commented-out code
|
11 | # This is a real comment.
12 | # # This is a (nested) comment.
@@ -82,7 +82,7 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
10 10 |
11 11 | # This is a real comment.
12 12 | # # This is a (nested) comment.
@@ -91,7 +91,7 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code
15 14 |
16 15 | #import os # noqa: ERA001
ERA001.py:21:5: ERA001 [*] Found commented-out code
ERA001.py:21:5: ERA001 Found commented-out code
|
19 | class A():
20 | pass
@@ -100,7 +100,7 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
18 18 |
19 19 | class A():
20 20 | pass
@@ -109,7 +109,7 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
23 22 |
24 23 | dictionary = {
ERA001.py:26:5: ERA001 [*] Found commented-out code
ERA001.py:26:5: ERA001 Found commented-out code
|
24 | dictionary = {
25 | # "key1": 123, # noqa: ERA001
@@ -120,7 +120,7 @@ ERA001.py:26:5: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
23 23 |
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
@@ -129,7 +129,7 @@ ERA001.py:26:5: ERA001 [*] Found commented-out code
28 27 | }
29 28 |
ERA001.py:27:5: ERA001 [*] Found commented-out code
ERA001.py:27:5: ERA001 Found commented-out code
|
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
@@ -139,7 +139,7 @@ ERA001.py:27:5: ERA001 [*] Found commented-out code
|
= help: Remove commented-out code
Manual fix
Possible fix
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 26 | # "key2": 456,

View File

@@ -11,7 +11,7 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::registry::Rule;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
/// ## What it does
@@ -702,12 +702,10 @@ pub(crate) fn definition(
},
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic_unsafe(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
)));
}
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
)));
diagnostics.push(diagnostic);
}
}
@@ -719,13 +717,11 @@ pub(crate) fn definition(
},
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.set_fix(Fix::automatic_unsafe(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
}
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
}
diagnostics.push(diagnostic);
}

View File

@@ -252,7 +252,7 @@ annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
156 156 |
157 157 | class Foo:
158 158 | @decorator()
@@ -272,7 +272,7 @@ annotation_presence.py:165:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
162 162 |
163 163 | # Regression test for: https://github.com/astral-sh/ruff/issues/7711
164 164 | class Class:

View File

@@ -11,7 +11,7 @@ mypy_init_return.py:5:9: ANN204 [*] Missing return type annotation for special m
|
= help: Add `None` return type
Unsafe fix
Suggested fix
2 2 |
3 3 | # Error
4 4 | class Foo:
@@ -31,7 +31,7 @@ mypy_init_return.py:11:9: ANN204 [*] Missing return type annotation for special
|
= help: Add `None` return type
Unsafe fix
Suggested fix
8 8 |
9 9 | # Error
10 10 | class Foo:
@@ -59,7 +59,7 @@ mypy_init_return.py:47:9: ANN204 [*] Missing return type annotation for special
|
= help: Add `None` return type
Unsafe fix
Suggested fix
44 44 | # Error used to be ok for a moment since the mere presence
45 45 | # of a vararg falsely indicated that the function has a typed argument.
46 46 | class Foo:

View File

@@ -10,7 +10,7 @@ simple_magic_methods.py:2:9: ANN204 [*] Missing return type annotation for speci
|
= help: Add `None` return type
Unsafe fix
Suggested fix
1 1 | class Foo:
2 |- def __str__(self):
2 |+ def __str__(self) -> str:
@@ -28,7 +28,7 @@ simple_magic_methods.py:5:9: ANN204 [*] Missing return type annotation for speci
|
= help: Add `None` return type
Unsafe fix
Suggested fix
2 2 | def __str__(self):
3 3 | ...
4 4 |
@@ -48,7 +48,7 @@ simple_magic_methods.py:8:9: ANN204 [*] Missing return type annotation for speci
|
= help: Add `None` return type
Unsafe fix
Suggested fix
5 5 | def __repr__(self):
6 6 | ...
7 7 |
@@ -68,7 +68,7 @@ simple_magic_methods.py:11:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
8 8 | def __len__(self):
9 9 | ...
10 10 |
@@ -88,7 +88,7 @@ simple_magic_methods.py:14:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
11 11 | def __length_hint__(self):
12 12 | ...
13 13 |
@@ -108,7 +108,7 @@ simple_magic_methods.py:17:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
14 14 | def __init__(self):
15 15 | ...
16 16 |
@@ -128,7 +128,7 @@ simple_magic_methods.py:20:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
17 17 | def __del__(self):
18 18 | ...
19 19 |
@@ -148,7 +148,7 @@ simple_magic_methods.py:23:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
20 20 | def __bool__(self):
21 21 | ...
22 22 |
@@ -168,7 +168,7 @@ simple_magic_methods.py:26:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
23 23 | def __bytes__(self):
24 24 | ...
25 25 |
@@ -188,7 +188,7 @@ simple_magic_methods.py:29:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
26 26 | def __format__(self, format_spec):
27 27 | ...
28 28 |
@@ -208,7 +208,7 @@ simple_magic_methods.py:32:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
29 29 | def __contains__(self, item):
30 30 | ...
31 31 |
@@ -228,7 +228,7 @@ simple_magic_methods.py:35:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
32 32 | def __complex__(self):
33 33 | ...
34 34 |
@@ -248,7 +248,7 @@ simple_magic_methods.py:38:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
35 35 | def __int__(self):
36 36 | ...
37 37 |
@@ -268,7 +268,7 @@ simple_magic_methods.py:41:9: ANN204 [*] Missing return type annotation for spec
|
= help: Add `None` return type
Unsafe fix
Suggested fix
38 38 | def __float__(self):
39 39 | ...
40 40 |

View File

@@ -12,6 +12,7 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
| "assertNotEquals"
| "bool"
| "bytes"
| "coalesce"
| "count"
| "failIfEqual"
| "failUnlessEqual"
@@ -22,12 +23,15 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
| "getboolean"
| "getfloat"
| "getint"
| "ifnull"
| "index"
| "insert"
| "int"
| "is_"
| "is_not"
| "isnull"
| "next"
| "nvl"
| "param"
| "pop"
| "remove"

View File

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

View File

@@ -6,7 +6,6 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_false;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `assert False`.
@@ -75,11 +74,9 @@ pub(crate) fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg:
}
let mut diagnostic = Diagnostic::new(AssertFalse, test.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic_unsafe(Edit::range_replacement(
checker.generator().stmt(&assertion_error(msg)),
stmt.range(),
)));
}
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().stmt(&assertion_error(msg)),
stmt.range(),
)));
checker.diagnostics.push(diagnostic);
}

View File

@@ -11,7 +11,7 @@ use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::registry::{AsRule, Rule};
use crate::registry::Rule;
/// ## What it does
/// Checks for `try-except` blocks with duplicate exception handlers.
@@ -146,24 +146,22 @@ fn duplicate_handler_exceptions<'a>(
},
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic_safe(Edit::range_replacement(
// Single exceptions don't require parentheses, but since we're _removing_
// parentheses, insert whitespace as needed.
if let [elt] = unique_elts.as_slice() {
pad(
checker.generator().expr(elt),
expr.range(),
checker.locator(),
)
} else {
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
)));
}
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
// Single exceptions don't require parentheses, but since we're _removing_
// parentheses, insert whitespace as needed.
if let [elt] = unique_elts.as_slice() {
pad(
checker.generator().expr(elt),
expr.range(),
checker.locator(),
)
} else {
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -6,7 +6,6 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `getattr` that take a constant attribute value as an
@@ -85,26 +84,24 @@ pub(crate) fn getattr_with_constant(
}
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic_unsafe(Edit::range_replacement(
pad(
if matches!(
obj,
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
) && !checker.locator().contains_line_break(obj.range())
{
format!("{}.{}", checker.locator().slice(obj), value)
} else {
// Defensively parenthesize any other expressions. For example, attribute accesses
// on `int` literals must be parenthesized, e.g., `getattr(1, "real")` becomes
// `(1).real`. The same is true for named expressions and others.
format!("({}).{}", checker.locator().slice(obj), value)
},
expr.range(),
checker.locator(),
),
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
pad(
if matches!(
obj,
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
) && !checker.locator().contains_line_break(obj.range())
{
format!("{}.{}", checker.locator().slice(obj), value)
} else {
// Defensively parenthesize any other expressions. For example, attribute accesses
// on `int` literals must be parenthesized, e.g., `getattr(1, "real")` becomes
// `(1).real`. The same is true for named expressions and others.
format!("({}).{}", checker.locator().slice(obj), value)
},
expr.range(),
)));
}
checker.locator(),
),
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}

View File

@@ -11,7 +11,6 @@ use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of mutable objects as function argument defaults.
@@ -110,18 +109,16 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
// If the function body is on the same line as the function def, do not fix
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = move_initialization(
function_def,
parameter,
default,
checker.locator(),
checker.stylist(),
checker.indexer(),
checker.generator(),
) {
diagnostic.set_fix(fix);
}
if let Some(fix) = move_initialization(
function_def,
parameter,
default,
checker.locator(),
checker.stylist(),
checker.indexer(),
checker.generator(),
) {
diagnostic.set_fix(fix);
}
checker.diagnostics.push(diagnostic);
}
@@ -200,5 +197,5 @@ fn move_initialization(
}
let initialization_edit = Edit::insertion(content, pos);
Some(Fix::manual_edits(default_edit, [initialization_edit]))
Some(Fix::display_edits(default_edit, [initialization_edit]))
}

View File

@@ -6,7 +6,6 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::registry::AsRule;
/// ## What it does
/// Checks for single-element tuples in exception handlers (e.g.,
@@ -77,23 +76,21 @@ pub(crate) fn redundant_tuple_in_exception_handler(
},
type_.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// If there's no space between the `except` and the tuple, we need to insert a space,
// as in:
// ```python
// except(ValueError,):
// ```
// Otherwise, the output will be invalid syntax, since we're removing a set of
// parentheses.
diagnostic.set_fix(Fix::automatic_safe(Edit::range_replacement(
pad(
checker.generator().expr(elt),
type_.range(),
checker.locator(),
),
// If there's no space between the `except` and the tuple, we need to insert a space,
// as in:
// ```python
// except(ValueError,):
// ```
// Otherwise, the output will be invalid syntax, since we're removing a set of
// parentheses.
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
pad(
checker.generator().expr(elt),
type_.range(),
)));
}
checker.locator(),
),
type_.range(),
)));
checker.diagnostics.push(diagnostic);
}
}

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