Compare commits

..

69 Commits

Author SHA1 Message Date
Zanie
ab6f173bc1 Add tag ltieral 2023-09-15 14:55:37 -05:00
Zanie
500d1b854b Demo full changes 2023-09-15 14:54:24 -05:00
Zanie
1c0c00d22d Unshallow main 2023-09-15 14:39:00 -05:00
Zanie
675cc216cb Test an older commit 2023-09-15 14:38:02 -05:00
Zanie
3fcfa50212 Try checking out main isntead of a the commit 2023-09-15 14:36:55 -05:00
Zanie
bf0d645692 Remove unshallow 2023-09-15 14:32:54 -05:00
Zanie
f392791b69 Fix depth 2023-09-15 14:31:53 -05:00
Zanie
b015a06966 Use a full clone 2023-09-15 14:29:24 -05:00
Zanie Blue
8cf0708b96 Remove branch 2023-09-15 14:27:09 -05:00
Zanie
3bc593ad7f DEBUG: Test main branch check for release 2023-09-15 14:22:49 -05:00
Zanie Blue
0c030b5bf3 Bump version to 0.0.290 (#7413)
See also:
- https://github.com/astral-sh/astral-sh/pull/41
- https://github.com/astral-sh/ruff-pre-commit/pull/51
2023-09-15 13:51:46 -05:00
Zanie Blue
1b082ce67e Add maximum length for line-length to JSON schema (#7412)
<!--
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? -->
Adds the maximum of 320 for the line-length setting to the JSON schema
for better integration with IDEs.

Related https://github.com/astral-sh/ruff/pull/6873

## Test Plan

<!-- How was it tested? -->
2023-09-15 18:10:06 +00:00
dependabot[bot]
f936d319cc Bump toml from 0.7.6 to 0.7.8 (#7405)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 09:14:58 +00:00
dependabot[bot]
85d8b6228f Bump chrono from 0.4.28 to 0.4.30 (#7406)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 09:00:17 +00:00
dependabot[bot]
7594dadc1d Bump syn from 2.0.29 to 2.0.33 (#7404)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 08:49:52 +00:00
dependabot[bot]
de37fbfac9 Bump serde-wasm-bindgen from 0.5.0 to 0.6.0 (#7403)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 08:47:42 +00:00
dependabot[bot]
4e2769a16c Bump mimalloc from 0.1.38 to 0.1.39 (#7402)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 08:46:55 +00:00
Manuel Jacob
75b5c314e3 Change CWE reference in documentation for S607 rule (#7398)
<!--
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

The previous reference was “CWE-78: Improper Neutralization of Special
Elements used in an OS Command ('OS Command Injection')”, which
describes another issue. The new reference is “CWE-426: Untrusted Search
Path”, which describes exactly the problem that this rule should warn
about.

## Test Plan

The change was not tested, as it only changes two numbers in the
documentation.
2023-09-14 23:12:54 -05:00
Charlie Marsh
450fb9b99a [flake8-logging] Implement LOG001: direct-logger-instantiation (#7397)
## Summary

See: https://github.com/astral-sh/ruff/issues/7248.
2023-09-14 23:07:10 -04:00
Charlie Marsh
6163c99551 Mark PERF403 as a preview rule (#7396) 2023-09-15 01:57:48 +00:00
qdegraaf
3112202a5b [flake8-logging] Add flake8_logging boilerplate and first rule LOG009 (#7249)
## Summary

Adds `LOG009` from
[flake8-logging](https://github.com/adamchainz/flake8-logging). Also
adds the boilerplate for a new plugin

Checks for usages of undocumented `logging.WARN` constant and suggests
replacement with `logging.WARNING`.

## Test Plan

`cargo test` with fresh fixture

## Issue links

Refers: https://github.com/astral-sh/ruff/issues/7248
2023-09-15 01:41:32 +00:00
Charlie Marsh
64ea00048b Add known problems to UP040 documentation (#7395) 2023-09-15 01:31:20 +00:00
qdegraaf
067a4acd54 [perflint] Add PERF403 (#6132)
## Summary

Adds `PERF403` mirroring `W8403` from
https://github.com/tonybaloney/perflint

## Test Plan

Fixtures were added based on perflint tests

## Issue Links

Refers: https://github.com/astral-sh/ruff/issues/4789
2023-09-15 01:23:51 +00:00
Nathan Whitaker
c88376f468 Add support for bounds, constraints, and explicit variance on generic type variables to UP040 (#6749)
## Summary

Extends UP040 to support moving type variables with
bounds/constraints/variance that are used in type aliases to use PEP-695
syntax.

Part of #4617.

## Test Plan

The existing tests added by #6314 already cover the relevant cases.
2023-09-14 21:11:24 -04:00
Charlie Marsh
9b7c29853d Reflect conversion reason in UP012 messages (#7393)
Closes https://github.com/astral-sh/ruff/issues/7254.
2023-09-14 20:08:58 +00:00
Charlie Marsh
3e21d32b79 Avoid flagging single-quoted docstrings with continuations for multi-line rules (#7392)
Rules like D209 and D205 are only intended to apply to multi-line
docstrings. If a docstring is single-quoted, but extends via a
continuation, it should be excluded (it'll be flagged by another rule
anyway). Closes https://github.com/astral-sh/ruff/issues/7058.
2023-09-14 18:58:39 +00:00
Charlie Marsh
f9e3ea23ba Show rule codes in shell tab completion (#7375)
## Summary

I noticed that we have a custom parser for rule selectors, but it wasn't
actually being used? This PR adds it back to our Clap setup and changes
the parser to only show full categories and individual rules when
tab-completing:

<img width="1792" alt="Screen Shot 2023-09-13 at 9 13 38 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/028b18d2-8c92-49c1-b781-f24c9ae310f7">

<img width="1792" alt="Screen Shot 2023-09-13 at 9 13 40 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/fd598da5-78fb-412d-a69e-2a0963d479cd">

<img width="1792" alt="Screen Shot 2023-09-13 at 9 13 58 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/7c482b90-6e54-425c-ae23-fb50496a177a">

The previous implementation showed all codes, which I found too noisy:

<img width="1792" alt="Screen Shot 2023-09-13 at 8 57 09 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/db370a0e-2a9f-4acd-b1e3-224a1f8e9ce5">
2023-09-14 18:37:23 +00:00
Charlie Marsh
6856d0b44b Use dot references in docs for methods (#7391)
## Summary

This matches the convention used in the Python documentation.
2023-09-14 14:35:34 -04:00
Charlie Marsh
21539f1663 Allow NURSERY in JSON Schema (#7374)
## Summary

At some point, we removed these so that they wouldn't be autocompleted
for users, since we wanted to discourage usage of `ALL`. But given that
they're valid values, I think that was a bad idea -- it leads to an even
more confusing experience whereby JSON Schema validators tell you that
you have an error, when you don't.

Closes https://github.com/astral-sh/ruff/issues/7261.
2023-09-14 18:09:35 +00:00
Zanie Blue
b9bb6bf780 Remove the PREVIEW rule selector (#7389)
The rule selector is not useful because `--select PREVIEW` only targets
Ruff developers and `--ignore PREVIEW` has no effect due to its low
specificity. We may restore it later if useful.
2023-09-14 12:31:59 -05:00
Charlie Marsh
d39eae2713 Extend C416 to catch tuple unpacking (#7363)
Closes https://github.com/astral-sh/ruff/issues/7307.
2023-09-14 15:53:15 +00:00
Charlie Marsh
5d21b9c22e Catch panics in formatter (#7377)
## Summary

This PR ensures that we catch and render panics in the formatter
identically to other kinds of errors. It also improves the consistency
in error rendering throughout and makes a few stylistic changes to the
messages.

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

## Test Plan

I created a file `foo.py` with a syntax error, and a file `bar.py` with
an intentional panic.

<img width="1624" alt="Screen Shot 2023-09-13 at 10 25 22 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/605c2839-ad02-4376-a2e9-d5a593ab660f">

<img width="1624" alt="Screen Shot 2023-09-13 at 10 25 24 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/b1381909-157c-48cb-9630-d0bbfcb1b640">
2023-09-14 11:44:16 -04:00
Charlie Marsh
ec2f229a45 Remove ExprContext from ComparableExpr (#7362)
`ComparableExpr` includes the `ExprContext` field on an expression, so,
e.g., the two tuples in `(a, b) = (a, b)` won't be considered equal.
Similarly, the tuples in `[(a, b) for (a, b) in c]` _also_ wouldn't be
considered equal. I find this behavior surprising, since
`ComparableExpr` is intended to allow you to compare two ASTs, but
`ExprContext` is really encoding information about the broader context
for the expression.
2023-09-14 15:40:02 +00:00
Charlie Marsh
34c1cb7d11 Treat parenthesized power operands as non-simple (#7371)
Closes https://github.com/astral-sh/ruff/issues/7318.
2023-09-14 15:36:21 +00:00
dependabot[bot]
2ddea7c657 Bump regex from 1.9.4 to 1.9.5 (#7384)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 14:58:24 +00:00
dependabot[bot]
45eabdd2c3 Bump shlex from 1.1.0 to 1.2.0 (#7381)
Bumps [shlex](https://github.com/comex/rust-shlex) from 1.1.0 to 1.2.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/comex/rust-shlex/blob/master/CHANGELOG.md">shlex's
changelog</a>.</em></p>
<blockquote>
<h1>1.2.0</h1>
<ul>
<li>Adds <code>bytes</code> module to support operating directly on byte
strings.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/comex/rust-shlex/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=shlex&package-manager=cargo&previous-version=1.1.0&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-14 09:40:05 -05:00
Micha Reiser
675c86c175 fix: Group fluent subscript (#7386) 2023-09-14 13:04:14 +02:00
dependabot[bot]
1f8e2b8f14 Bump proc-macro2 from 1.0.66 to 1.0.67 (#7383)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 09:49:57 +00:00
Micha Reiser
58b3040342 chore: Upgrade notify (#7338) 2023-09-14 09:47:55 +00:00
dependabot[bot]
6a9b8aede1 Bump thiserror from 1.0.47 to 1.0.48 (#7385)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 09:21:47 +00:00
dependabot[bot]
f48126ad00 Bump clap from 4.4.1 to 4.4.3 (#7382)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-14 09:13:12 +00:00
Charlie Marsh
11287f944f Avoid re-parenthesizing call chains whose inner values are parenthesized (#7373)
## Summary

Given a statement like:

```python
result = (
    f(111111111111111111111111111111111111111111111111111111111111111111111111111111111)
    + 1
)()
```

When we go to parenthesize the target of the assignment, we use
`maybe_parenthesize_expression` with `Parenthesize::IfBreaks`. This then
checks if the call on the right-hand side needs to be parenthesized, the
implementation of which looks like:

```rust
impl NeedsParentheses for ExprCall {
    fn needs_parentheses(
        &self,
        _parent: AnyNodeRef,
        context: &PyFormatContext,
    ) -> OptionalParentheses {
        if CallChainLayout::from_expression(self.into(), context.source())
            == CallChainLayout::Fluent
        {
            OptionalParentheses::Multiline
        } else if context.comments().has_dangling(self) {
            OptionalParentheses::Always
        } else {
            self.func.needs_parentheses(self.into(), context)
        }
    }
}
```

Checking for `self.func.needs_parentheses(self.into(), context)` is
problematic, since, as in the example above, `self.func` may _already_
be parenthesized -- in which case, we _don't_ want to parenthesize the
entire expression. If we do, we end up with this non-ideal formatting:

```python
result = (
    (
        f(
            111111111111111111111111111111111111111111111111111111111111111111111111111111111
        )
        + 1
    )()
)
```

This PR modifies the `NeedsParentheses` implementations for call chain
expressions to return `Never` if the inner expression has its own
parentheses, in which case, the formatting implementations for those
expressions will preserve them anyway.

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

## Test Plan

Zulip improves a bit, everything else is unchanged.

Before:

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

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99981 | 2760 | 40 |
| transformers | 0.99944 | 2587 | 413 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99834 | 648 | 20 |
| zulip | 0.99956 | 1437 | 23 |

After:

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

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99981 | 2760 | 40 |
| transformers | 0.99944 | 2587 | 413 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99834 | 648 | 20 |
| **zulip** | **0.99962** | **1437** | **22** |
2023-09-14 05:05:37 -04:00
Micha Reiser
a65efcf459 fix: Don't omit optional parentheses for subscripts (#7380) 2023-09-14 08:43:53 +00:00
Jelle van der Waa
04183b0299 [pylint] Implement too-many-public-methods rule (PLR0904) (#6179)
Implement
https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/too-many-public-methods.html

Confusingly the rule page mentions a max of 7 while in practice it is
20.
https://github.com/search?q=repo%3Apylint-dev%2Fpylint+max-public-methods&type=code

## Summary

Implement pylint's R0904

## Test Plan

Unit tests.
2023-09-14 00:52:26 +00:00
James Braza
36fa1fe359 Docs linking error tutorial with error suppression (#7014)
Documents takeaway from
https://github.com/astral-sh/ruff/discussions/7011#discussioncomment-6869239.
2023-09-13 20:22:18 -04:00
Charlie Marsh
6e625bd93d Invert reverse argument regardless of whether it's a boolean (#7372)
## Summary

When fixing `reversed(sorted(x, reverse=False))`, we rewrite as
`sorted(x, reverse=True)`. However, if the `reverse` argument isn't
`True` or `False`, we leave it as-is, which is incorrect.

Now, given `reversed(sorted(x, reverse=y))`, we rewrite as `sorted(x,
reverse=not y)`.
2023-09-13 20:12:35 -04:00
Zanie Blue
ebd1b296fd Add warnings for nursery and preview rule selection (#7210)
## Summary

Adds warnings for cases where:
- A selector does not include any rules because preview is disabled
- A nursery rule is selected without the preview flag

## Test plan

Add integration tests
2023-09-13 15:29:58 -05:00
Zanie Blue
1373e1c395 Update release workflow to checkout the given sha (#7279) 2023-09-13 14:59:41 -05:00
Zanie Blue
4bff397318 Move FURB145 from nursery to preview (#7364)
Moves the new rule from nursery to preview for the upcoming release.

Adds new test coverage for selection of a single preview rule and fixes
a bug where preview rules were incorrectly selectable with exact codes.
2023-09-13 14:54:28 -05:00
Charlie Marsh
5347df4728 Parenthesize single-generator arguments when adding reverse keyword (#7365)
Closes https://github.com/astral-sh/ruff/issues/7289.
2023-09-13 19:40:45 +00:00
Tom Kuson
ebe9c03545 [refurb] Implement no-slice-copy (FURB145) (#7007)
## Summary

Implement
[`no-slice-copy`](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/no_slice_copy.py)
as `slice-copy` (`FURB145`).

Related to #1348.

## Test Plan

`cargo test`
2023-09-13 17:31:15 +00:00
Charlie Marsh
b0cbcd3dfa Update deprecated-import lists based on recent typing-extension release (#7356)
## Summary

Generated by running
f3cff244e3/testing/generate-typing-rewrite-info (L14)
with latest `typing-extensions` and manually applying the changes.

Closes https://github.com/astral-sh/ruff/issues/7324.
2023-09-13 13:21:11 -04:00
Charlie Marsh
4df9e07a79 Add a benchmarking script for the formatter CLI (#7340)
## Summary

This PR adds a benchmarking script for the formatter, which benchmarks
the Ruff formatter against Black, yapf, and autopep8.

Three benchmarks are included:

1. Format everything.
2. Format everything, but use a single thread.
3. Format everything, but `--check` (don't write to disk).

There's some nuance in figuring out the right combination of arguments
to each command, but the _main_ nuance is to ensure that we always run
the given formatter (and modify the target repo in-place) prior to
benchmarking it, so that the formatters aren't disadvantaged by the
existing formatting of the target repo. (E.g.: prior to benchmarking
Black's preview style, we need to make sure we format the target repo
with Black's preview style; otherwise, preview style appears much
slower.)

Part of https://github.com/astral-sh/ruff/issues/7309.
2023-09-13 13:15:48 -04:00
Charlie Marsh
f0f7ea7502 Treat whitespace-only line as blank for D411 (#7351)
This better aligns with the definition of "blank line" that we use
throughout the docstring rules.

Closes https://github.com/astral-sh/ruff/issues/7216.
2023-09-13 16:41:12 +00:00
Micha Reiser
4f26002dd5 chore: Upgrade strum (#7337)
## Summary

This PR upgrades `strum` from 0.24.x to 0.25.x. 

The breaking changes are: 
* strum macros now uses syn2
* The `to_string` behavior changed when using `default`. I did a quick search, we aren't using `strum(default)` 


`strum` now has a `#[derive(EnumIs)]` macro that generates `is_` methods. 

## Test Plan

cargo test
2023-09-13 18:33:27 +02:00
dependabot[bot]
d1a9c198e3 Bump path-absolutize from 3.1.0 to 3.1.1 (#7345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 16:21:51 +00:00
dependabot[bot]
7a4f699fba Bump argfile from 0.1.5 to 0.1.6 (#7344)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 16:18:21 +00:00
dependabot[bot]
3fb5418c2c Bump memchr from 2.6.2 to 2.6.3 (#7343)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 18:14:42 +02:00
dependabot[bot]
9fcc009a0c Bump serde_json from 1.0.105 to 1.0.106 (#7342)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 18:13:24 +02:00
Micha Reiser
bf8e5a167b chore: Upgrade walkdir (#7336)
## Summary

The only commit is [api: add follow_root_links() option to WalkDir](dcc527d832) whicih addsa  new option wheter `walkdir` should follow a root symlink or not. 
The new option defaults to `true` which is the same as before. 

## Test Plan

`cargo test`
2023-09-13 18:07:24 +02:00
Micha Reiser
8a001dfc3d chore: Upgrade pyproject-toml crate (#7335)
## Summary

This PR bumps the pyproject-toml crate to 0.7.0. The only difference is that it now depends on indexmap 2. I reviewed the indexmap 2 changes and they don't seem relevant to us. 

I used this opportunity to remove the default features from `serde_with` which removes our indexmap 1 dependency (and some other unused dependencies)

## Test Plan

`cargo test`
2023-09-13 17:55:03 +02:00
Zanie Blue
0823394525 Display nursery rules as preview in documentation (#7341)
This is broken in the last release
2023-09-13 10:46:43 -05:00
Zanie Blue
e15047815c Update Rust toolchain file to use TOML format (#7339)
Ref https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file
Should resolve Dependabot failure at
https://github.com/astral-sh/ruff/network/updates/721380342
Follows #7034
2023-09-13 15:43:03 +00:00
Micha Reiser
7531bb3b21 Upgrade is-macros to 0.3.0 (#7334) 2023-09-13 15:27:40 +02:00
Micha Reiser
2d9b39871f Introduce IndentWidth (#7301) 2023-09-13 14:52:24 +02:00
Micha Reiser
e122a96d27 playground: Respect line-length and preview configuration (#7330) 2023-09-13 12:14:25 +00:00
konsti
f4c7bff36b Don't reorder parameters in function calls (#7268)
## Summary

In `f(*args, a=b, *args2, **kwargs)` the args (`*args`, `*args2`) and
keywords (`a=b`, `**kwargs`) are interleaved, which we previously didn't
handle.

Fixes #6498

**main**

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

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99966 | 2760 | 58 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |

**PR**

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

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99967 | 2760 | 53 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |


## Test Plan

New fixtures
2023-09-13 09:01:49 +00:00
konsti
56440ad835 Introduce ArgOrKeyword to keep call parameter order (#7302)
## Motivation

The `ast::Arguments` for call argument are split into positional
arguments (args) and keywords arguments (keywords). We currently assume
that call consists of first args and then keywords, which is generally
the case, but not always:

```python
f(*args, a=2, *args2, **kwargs)

class A(*args, a=2, *args2, **kwargs):
    pass
```

The consequence is accidentally reordering arguments
(https://github.com/astral-sh/ruff/pull/7268).

## Summary

`Arguments::args_and_keywords` returns an iterator of an `ArgOrKeyword`
enum that yields args and keywords in the correct order. I've fixed the
obvious `args` and `keywords` usages, but there might be some cases with
wrong assumptions remaining.

## Test Plan

The generator got new test cases, otherwise the stacked PR
(https://github.com/astral-sh/ruff/pull/7268) which uncovered this.
2023-09-13 08:45:46 +00:00
Charlie Marsh
179128dc54 Link discussion in formatter README (#7311) 2023-09-12 16:50:22 +00:00
164 changed files with 3644 additions and 1888 deletions

View File

@@ -366,3 +366,31 @@ jobs:
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
tmp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main # We checkout the main branch to check for the commit
- name: Check main branch
run: |
# Fetch the main branch since a shallow checkout is used by default
git fetch origin main --unshallow
if ! git branch --contains 0c030b5bf31e425cb6070db7386243eca6dbd8f1 | grep -E '(^|\s)main$'; then
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check tag consistency
run: |
# Switch to the commit we want to release
git checkout 0c030b5bf31e425cb6070db7386243eca6dbd8f1
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "0.0.290" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "0.0.290" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi

View File

@@ -7,12 +7,15 @@ on:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "Optionally, the full sha of the commit to be released"
description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
default: ""
type: string
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
- pyproject.toml
# And when we change this workflow itself...
- .github/workflows/release.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -31,6 +34,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -57,6 +62,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -95,6 +102,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -141,6 +150,8 @@ jobs:
arch: x64
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -187,6 +198,8 @@ jobs:
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -244,6 +257,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -297,6 +312,8 @@ jobs:
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -351,6 +368,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -399,6 +418,8 @@ jobs:
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
@@ -410,6 +431,15 @@ jobs:
else
echo "Releasing ${version}"
fi
- name: Check main branch
if: ${{ inputs.sha }}
run: |
# Fetch the main branch since a shallow checkout is used by default
git fetch origin main --unshallow
if ! git branch --contains ${{ inputs.sha }} | grep -E '(^|\s)main$'; then
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check SHA consistency
if: ${{ inputs.sha }}
run: |
@@ -465,6 +495,8 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: git tag
run: |
git config user.email "hey@astral.sh"

259
Cargo.lock generated
View File

@@ -128,10 +128,11 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "argfile"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265f5108974489a217d5098cd81666b60480c8dd67302acbbe7cbdd8aa09d638"
checksum = "1287c4f82a41c5085e65ee337c7934d71ab43d5187740a81fb69129013f6a5f6"
dependencies = [
"fs-err",
"os_str_bytes",
]
@@ -220,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"regex-automata 0.3.7",
"regex-automata 0.3.8",
"serde",
]
@@ -271,16 +272,14 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.28"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f"
checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"time 0.1.45",
"wasm-bindgen",
"windows-targets 0.48.5",
]
@@ -314,20 +313,19 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.1"
version = "4.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstream",
"anstyle",
@@ -378,14 +376,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.4.0"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a"
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -610,7 +608,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -621,16 +619,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.29",
]
[[package]]
name = "deranged"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
dependencies = [
"serde",
"syn 2.0.33",
]
[[package]]
@@ -821,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.289"
version = "0.0.290"
dependencies = [
"anyhow",
"clap",
@@ -892,7 +881,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"wasm-bindgen",
]
@@ -945,12 +934,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexf-parse"
version = "0.2.1"
@@ -1039,17 +1022,6 @@ dependencies = [
"rust-stemmers",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
@@ -1140,15 +1112,15 @@ dependencies = [
[[package]]
name = "is-macro"
version = "0.2.2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20"
checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e"
dependencies = [
"Inflector",
"pmutil",
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.33",
]
[[package]]
@@ -1303,9 +1275,9 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.34"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d058a81af0d1c22d7a1c948576bee6d673f7af3c0f35564abd6c81122f513d"
checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664"
dependencies = [
"cc",
"libc",
@@ -1356,9 +1328,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "memoffset"
@@ -1371,9 +1343,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.38"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "972e5f23f6716f62665760b0f4cbf592576a80c7b879ba9beaafc0e558894127"
checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c"
dependencies = [
"libmimalloc-sys",
]
@@ -1401,7 +1373,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.48.0",
]
@@ -1446,20 +1418,21 @@ dependencies = [
[[package]]
name = "notify"
version = "5.2.0"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"walkdir",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1582,18 +1555,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "path-absolutize"
version = "3.1.0"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de"
checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.1.0"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e"
checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
dependencies = [
"once_cell",
]
@@ -1672,7 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap 2.0.0",
"indexmap",
]
[[package]]
@@ -1739,6 +1712,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "pmutil"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
]
[[package]]
name = "portable-atomic"
version = "1.4.3"
@@ -1821,20 +1805,20 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.66"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyproject-toml"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee79feaa9d31e1c417e34219e610b67db4e786ce9b49d77dda549640abb9dc5f"
checksum = "569e259cd132eb8cec5df8b672d187c5260f82ad352156b5da9549d4472e64b0"
dependencies = [
"indexmap 1.9.3",
"indexmap",
"pep440_rs",
"pep508_rs",
"serde",
@@ -1848,7 +1832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
dependencies = [
"chrono",
"indexmap 2.0.0",
"indexmap",
"nextest-workspace-hack",
"quick-xml",
"thiserror",
@@ -1956,13 +1940,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.4"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.7",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
]
@@ -1977,9 +1961,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
@@ -2013,7 +1997,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fabf0a2e54f711c68c50d49f648a1a8a37adcb57353f518ac4df374f0788f42"
dependencies = [
"pmutil",
"pmutil 0.5.3",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -2037,7 +2021,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.289"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2135,7 +2119,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.289"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2275,7 +2259,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -2304,6 +2288,7 @@ dependencies = [
"bitflags 2.4.0",
"insta",
"is-macro",
"itertools",
"memchr",
"num-bigint",
"num-traits",
@@ -2688,9 +2673,9 @@ dependencies = [
[[package]]
name = "serde-wasm-bindgen"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
checksum = "30c9933e5689bd420dc6c87b7a1835701810cbc10cd86a26e4da45b73e6b1d78"
dependencies = [
"js-sys",
"serde",
@@ -2705,7 +2690,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -2721,9 +2706,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [
"itoa",
"ryu",
@@ -2754,15 +2739,8 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.28",
]
[[package]]
@@ -2774,7 +2752,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -2797,9 +2775,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "similar"
@@ -2852,24 +2830,24 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
"syn 2.0.33",
]
[[package]]
@@ -2885,9 +2863,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.29"
version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [
"proc-macro2",
"quote",
@@ -2992,22 +2970,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -3040,45 +3018,6 @@ dependencies = [
"tikv-jemalloc-sys",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
dependencies = [
"deranged",
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -3115,9 +3054,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.6"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
@@ -3136,11 +3075,11 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.14"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
@@ -3168,7 +3107,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -3371,7 +3310,7 @@ checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@@ -3430,20 +3369,14 @@ dependencies = [
[[package]]
name = "walkdir"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@@ -3471,7 +3404,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
"wasm-bindgen-shared",
]
@@ -3505,7 +3438,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@@ -14,39 +14,39 @@ license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
clap = { version = "4.1.8", features = ["derive"] }
chrono = { version = "0.4.30", default-features = false, features = ["clock"] }
clap = { version = "4.4.3", 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.31.0", feature = ["filters", "glob"] }
is-macro = { version = "0.2.2" }
is-macro = { version = "0.3.0" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
memchr = "2.5.0"
memchr = "2.6.3"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.0.14" }
proc-macro2 = { version = "1.0.51" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.67" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
regex = { version = "1.9.5" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93" }
serde_json = { version = "1.0.106" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.10.0" }
static_assertions = "1.1.0"
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
syn = { version = "2.0.15" }
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }
syn = { version = "2.0.33" }
test-case = { version = "3.0.0" }
thiserror = { version = "1.0.43" }
toml = { version = "0.7.2" }
thiserror = { version = "1.0.48" }
toml = { version = "0.7.8" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

25
LICENSE
View File

@@ -1224,6 +1224,31 @@ are:
SOFTWARE.
"""
- flake8-logging, licensed as follows:
"""
MIT License
Copyright (c) 2023 Adam Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- Pyright, licensed as follows:
"""
MIT License

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.289
rev: v0.0.290
hooks:
- id: ruff
```
@@ -274,6 +274,7 @@ quality tools, including:
- [flake8-gettext](https://pypi.org/project/flake8-gettext/)
- [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
- [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
- [flake8-logging](https://pypi.org/project/flake8-logging/)
- [flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
- [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420)
- [flake8-pie](https://pypi.org/project/flake8-pie/)

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.289"
version = "0.0.290"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -56,7 +56,7 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
pyproject-toml = { version = "0.6.0" }
pyproject-toml = { version = "0.7.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }

View File

@@ -7,6 +7,13 @@ reversed(sorted(x, reverse=True))
reversed(sorted(x, key=lambda e: e, reverse=True))
reversed(sorted(x, reverse=True, key=lambda e: e))
reversed(sorted(x, reverse=False))
reversed(sorted(x, reverse=x))
reversed(sorted(x, reverse=not x))
# Regression test for: https://github.com/astral-sh/ruff/issues/7289
reversed(sorted(i for i in range(42)))
reversed(sorted((i for i in range(42)), reverse=True))
def reversed(*args, **kwargs):
return None

View File

@@ -7,6 +7,8 @@ d = {"a": 1, "b": 2, "c": 3}
{i for i in x}
{k: v for k, v in y}
{k: v for k, v in d.items()}
[(k, v) for k, v in d.items()]
{k: (a, b) for k, (a, b) in d.items()}
[i for i, in z]
[i for i, j in y]

View File

@@ -0,0 +1,5 @@
import logging
logging.Logger(__name__)
logging.Logger()
logging.getLogger(__name__)

View File

@@ -0,0 +1,9 @@
import logging
logging.WARN # LOG009
logging.WARNING # OK
from logging import WARN, WARNING
WARN # LOG009
WARNING # OK

View File

@@ -0,0 +1,83 @@
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
result[idx] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # Ok (false negative: edge case where `else` is same as `if`)
else:
result[idx] = name
def foo():
result = {}
fruit = ["apple", "pear", "orange"]
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = []
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # OK (result is not a dictionary)
else:
result[idx] = name
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # OK (if/elif/else isn't replaceable)
elif idx % 3:
result[idx] = name
else:
result[idx] = name
def foo():
result = {1: "banana"}
fruit = ["apple", "pear", "orange"]
for idx, name in enumerate(fruit):
if idx % 2:
result[idx] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
if idx in result:
result[idx] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for name in fruit:
result[name] = name # PERF403
def foo():
fruit = ["apple", "pear", "orange"]
result = {}
for idx, name in enumerate(fruit):
result[name] = idx # PERF403

View File

@@ -658,3 +658,8 @@ class CommentAfterDocstring:
"After this docstring there's a comment." # priorities=1
def sort_services(self):
pass
def newline_after_closing_quote(self):
"We enforce a newline after the closing quote for a multi-line docstring \
but continuations shouldn't be considered multi-line"

View File

@@ -529,3 +529,16 @@ def replace_equals_with_dash2():
Parameters
===========
"""
@expect(_D213)
def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""

View File

@@ -0,0 +1,60 @@
class Everything:
foo = 1
def __init__(self):
pass
def _private(self):
pass
def method1(self):
pass
def method2(self):
pass
def method3(self):
pass
def method4(self):
pass
def method5(self):
pass
def method6(self):
pass
def method7(self):
pass
def method8(self):
pass
def method9(self):
pass
class Small:
def __init__(self):
pass
def _private(self):
pass
def method1(self):
pass
def method2(self):
pass
def method3(self):
pass
def method4(self):
pass
def method5(self):
pass
def method6(self):
pass

View File

@@ -74,6 +74,8 @@ from typing import Collection
from typing import AsyncGenerator
from typing import Reversible
from typing import Generator
from typing import Callable
from typing import cast
# OK
from a import b

View File

@@ -13,18 +13,19 @@ x: typing.TypeAlias = list[T]
T = typing.TypeVar("T")
x: typing.TypeAlias = list[T]
# UP040 bounded generic (todo)
# UP040 bounded generic
T = typing.TypeVar("T", bound=int)
x: typing.TypeAlias = list[T]
# UP040 constrained generic
T = typing.TypeVar("T", int, str)
x: typing.TypeAlias = list[T]
# UP040 contravariant generic (todo)
# UP040 contravariant generic
T = typing.TypeVar("T", contravariant=True)
x: typing.TypeAlias = list[T]
# UP040 covariant generic (todo)
# UP040 covariant generic
T = typing.TypeVar("T", covariant=True)
x: typing.TypeAlias = list[T]

View File

@@ -0,0 +1,21 @@
l = [1, 2, 3, 4, 5]
# Errors.
a = l[:]
b, c = 1, l[:]
d, e = l[:], 1
m = l[::]
l[:]
print(l[:])
# False negatives.
aa = a[:] # Type inference.
# OK.
t = (1, 2, 3, 4, 5)
f = t[:] # t.copy() is not supported.
g = l[1:3]
h = l[1:]
i = l[:3]
j = l[1:3:2]
k = l[::2]

View File

@@ -3,7 +3,7 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
@@ -92,10 +92,8 @@ pub(crate) fn remove_argument<T: Ranged>(
) -> Result<Edit> {
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.arguments_source_order()
.map(|arg| arg.range())
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());

View File

@@ -13,10 +13,10 @@ use crate::registry::Rule;
use crate::rules::{
flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging_format,
flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self, flake8_simplify,
flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet, pep8_naming, pycodestyle,
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet,
pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -113,10 +113,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, subscript);
}
if checker.enabled(Rule::InvalidIndexType) {
ruff::rules::invalid_index_type(checker, subscript);
}
if checker.settings.rules.enabled(Rule::SliceCopy) {
refurb::rules::slice_copy(checker, subscript);
}
pandas_vet::rules::subscript(checker, value, expr);
}
@@ -258,6 +260,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SixPY3) {
flake8_2020::rules::name_or_attribute(checker, expr);
}
if checker.enabled(Rule::UndocumentedWarn) {
flake8_logging::rules::undocumented_warn(checker, expr);
}
if checker.enabled(Rule::LoadBeforeGlobalDeclaration) {
pylint::rules::load_before_global_declaration(checker, id, expr);
}
@@ -324,6 +329,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
if checker.enabled(Rule::UndocumentedWarn) {
flake8_logging::rules::undocumented_warn(checker, expr);
}
pandas_vet::rules::attr(checker, attribute);
}
Expr::Call(
@@ -884,6 +892,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
if checker.enabled(Rule::DirectLoggerInstantiation) {
flake8_logging::rules::direct_logger_instantiation(checker, call);
}
}
Expr::Dict(ast::ExprDict {
keys,

View File

@@ -411,6 +411,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::EqWithoutHash) {
pylint::rules::object_without_hash_method(checker, class_def);
}
if checker.enabled(Rule::TooManyPublicMethods) {
pylint::rules::too_many_public_methods(
checker,
class_def,
checker.settings.pylint.max_public_methods,
);
}
if checker.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(checker, name);
}
@@ -1177,7 +1184,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
iter,
orelse,
is_async,
..
range: _,
},
) => {
if checker.any_enabled(&[
@@ -1211,6 +1218,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(checker, target, body);
}
if checker.enabled(Rule::ManualDictComprehension) {
perflint::rules::manual_dict_comprehension(checker, target, body);
}
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter);
}

View File

@@ -269,6 +269,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
#[allow(deprecated)]
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods),
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
#[allow(deprecated)]
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
@@ -897,6 +898,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy),
(Perflint, "403") => (RuleGroup::Preview, rules::perflint::rules::ManualDictComprehension),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme),
@@ -916,6 +918,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
// flake8-logging
(Flake8Logging, "001") => (RuleGroup::Preview, rules::flake8_logging::rules::DirectLoggerInstantiation),
(Flake8Logging, "009") => (RuleGroup::Preview, rules::flake8_logging::rules::UndocumentedWarn),
_ => return None,
})

View File

@@ -1,4 +1,6 @@
use libcst_native::{Expression, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace};
use libcst_native::{
Expression, Name, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace, UnaryOperation,
};
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
match expr {
@@ -50,3 +52,41 @@ pub(crate) fn or_space(whitespace: ParenthesizableWhitespace) -> Parenthesizable
whitespace
}
}
/// Negate a condition, i.e., `a` => `not a` and `not a` => `a`.
pub(crate) fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
if let Expression::UnaryOperation(ref expression) = expression {
if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) {
return *expression.expression.clone();
}
}
if let Expression::Name(ref expression) = expression {
match expression.value {
"True" => {
return Expression::Name(Box::new(Name {
value: "False",
lpar: vec![],
rpar: vec![],
}));
}
"False" => {
return Expression::Name(Box::new(Name {
value: "True",
lpar: vec![],
rpar: vec![],
}));
}
_ => {}
}
}
Expression::UnaryOperation(Box::new(UnaryOperation {
operator: libcst_native::UnaryOp::Not {
whitespace_after: space(),
},
expression: Box::new(expression.clone()),
lpar: vec![],
rpar: vec![],
}))
}

View File

@@ -30,6 +30,11 @@ impl<'a> Docstring<'a> {
pub(crate) fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
pub(crate) fn triple_quoted(&self) -> bool {
let leading_quote = self.leading_quote();
leading_quote.ends_with("\"\"\"") || leading_quote.ends_with("'''")
}
}
impl Ranged for Docstring<'_> {

View File

@@ -5,6 +5,8 @@
//!
//! [Ruff]: https://github.com/astral-sh/ruff
#[cfg(feature = "clap")]
pub use rule_selector::clap_completion::RuleSelectorParser;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::{IOError, SyntaxError};

View File

@@ -36,9 +36,6 @@ use crate::settings::{flags, Settings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
/// A [`Result`]-like type that returns both data and an error. Used to return
/// diagnostics even in the face of parse errors, since many diagnostics can be
/// generated without a full AST.
@@ -543,8 +540,9 @@ fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics:
let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule()));
if cfg!(debug_assertions) {
eprintln!(
"{}: Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
"debug error".red().bold(),
":".bold(),
MAX_ITERATIONS,
fs::relativize_path(path),
codes,
@@ -553,18 +551,17 @@ fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics:
} else {
eprintln!(
r#"
{}: Failed to converge after {} iterations.
{}{} Failed to converge after {} iterations.
This indicates a bug in `{}`. If you could open an issue at:
This indicates a bug in Ruff. If you could open an issue at:
{}/issues/new?title=%5BInfinite%20loop%5D
https://github.com/astral-sh/ruff/issues/new?title=%5BInfinite%20loop%5D
...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
":".bold(),
MAX_ITERATIONS,
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
codes
);
@@ -581,8 +578,9 @@ fn report_autofix_syntax_error(
let codes = collect_rule_codes(rules);
if cfg!(debug_assertions) {
eprintln!(
"{}: Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---",
"{}{} Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---",
"error".red().bold(),
":".bold(),
fs::relativize_path(path),
codes,
error,
@@ -591,17 +589,16 @@ fn report_autofix_syntax_error(
} else {
eprintln!(
r#"
{}: Autofix introduced a syntax error. Reverting all changes.
{}{} Autofix introduced a syntax error. Reverting all changes.
This indicates a bug in `{}`. If you could open an issue at:
This indicates a bug in Ruff. If you could open an issue at:
{}/issues/new?title=%5BAutofix%20error%5D
https://github.com/astral-sh/ruff/issues/new?title=%5BAutofix%20error%5D
...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
":".bold(),
fs::relativize_path(path),
codes,
);

View File

@@ -199,6 +199,9 @@ pub enum Linter {
/// [refurb](https://pypi.org/project/refurb/)
#[prefix = "FURB"]
Refurb,
/// [flake8-logging](https://pypi.org/project/flake8-logging/)
#[prefix = "LOG"]
Flake8Logging,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,

View File

@@ -15,10 +15,8 @@ use crate::settings::types::PreviewMode;
pub enum RuleSelector {
/// Select all rules (includes rules in preview if enabled)
All,
/// Category to select all rules in preview (includes legacy nursery rules)
Preview,
/// Legacy category to select all rules in the "nursery" which predated preview mode
#[deprecated(note = "Use `RuleSelector::Preview` for new rules instead")]
#[deprecated(note = "The nursery was replaced with 'preview mode' which has no selector")]
Nursery,
/// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters
/// via a single selector.
@@ -54,7 +52,6 @@ impl FromStr for RuleSelector {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"PREVIEW" => Ok(Self::Preview),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
@@ -121,7 +118,6 @@ impl RuleSelector {
RuleSelector::All => ("", "ALL"),
#[allow(deprecated)]
RuleSelector::Nursery => ("", "NURSERY"),
RuleSelector::Preview => ("", "PREVIEW"),
RuleSelector::C => ("", "C"),
RuleSelector::T => ("", "T"),
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
@@ -185,9 +181,6 @@ impl RuleSelector {
RuleSelector::Nursery => {
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
}
RuleSelector::Preview => RuleSelectorIter::Nursery(
Rule::iter().filter(|rule| rule.is_preview() || rule.is_nursery()),
),
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
@@ -212,7 +205,7 @@ impl RuleSelector {
// Always include rules that are not in preview or the nursery
!(rule.is_preview() || rule.is_nursery())
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|| (matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. }) && rule.is_nursery())
|| ((matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
// Enabling preview includes all preview or nursery rules
|| preview.is_enabled()
})
@@ -261,8 +254,9 @@ mod schema {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
[
// Include the non-standard "ALL" selector.
// Include the non-standard "ALL" and "NURSERY" selectors.
"ALL".to_string(),
"NURSERY".to_string(),
// Include the legacy "C" and "T" selectors.
"C".to_string(),
"T".to_string(),
@@ -301,7 +295,6 @@ impl RuleSelector {
pub fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
RuleSelector::Preview => Specificity::All,
#[allow(deprecated)]
RuleSelector::Nursery => Specificity::All,
RuleSelector::T => Specificity::LinterGroup,
@@ -343,13 +336,14 @@ pub enum Specificity {
}
#[cfg(feature = "clap")]
mod clap_completion {
pub mod clap_completion {
use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
use strum::IntoEnumIterator;
use crate::{
codes::RuleCodePrefix,
registry::{Linter, RuleNamespace},
rule_selector::is_single_rule_selector,
RuleSelector,
};
@@ -369,17 +363,29 @@ mod clap_completion {
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
value
.parse()
.map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))
value.parse().map_err(|_| {
let mut error =
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
error.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
error.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
error
})
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
@@ -394,27 +400,34 @@ mod clap_completion {
RuleCodePrefix::iter()
// Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is
// off-by-default
.filter(|p| {
format!("{}{}", p.linter().common_prefix(), p.short_code())
!= "RUF014"
.filter(|prefix| {
format!(
"{}{}",
prefix.linter().common_prefix(),
prefix.short_code()
) != "RUF014"
})
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
let mut rules_iter = p.rules();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();
let value = PossibleValue::new(format!("{prefix}{code}"));
if rule2.is_none() {
let rule1 = rule1.unwrap();
let name: &'static str = rule1.into();
value.help(name)
} else {
value
.filter_map(|prefix| {
// Ex) `UP`
if prefix.short_code().is_empty() {
let code = prefix.linter().common_prefix();
let name = prefix.linter().name();
return Some(PossibleValue::new(code).help(name));
}
// Ex) `UP004`
if is_single_rule_selector(&prefix) {
let rule = prefix.rules().next()?;
let code = format!(
"{}{}",
prefix.linter().common_prefix(),
prefix.short_code()
);
let name: &'static str = rule.into();
return Some(PossibleValue::new(code).help(name));
}
None
}),
),
),

View File

@@ -148,7 +148,7 @@ impl Violation for StartProcessWithNoShell {
///
/// ## References
/// - [Python documentation: `subprocess.Popen()`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen)
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
/// - [Common Weakness Enumeration: CWE-426](https://cwe.mitre.org/data/definitions/426.html)
#[violation]
pub struct StartProcessWithPartialPath;

View File

@@ -70,6 +70,7 @@ impl Violation for MutableArgumentDefault {
fn message(&self) -> String {
format!("Do not use mutable data structures for argument defaults")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `None`; initialize within function"))
}

View File

@@ -17,8 +17,8 @@ use crate::checkers::ast::Checker;
/// contains multiple characters, the reader may be misled into thinking that
/// a prefix or suffix is being removed, rather than a set of characters.
///
/// In Python 3.9 and later, you can use `str#removeprefix` and
/// `str#removesuffix` to remove an exact prefix or suffix from a string,
/// In Python 3.9 and later, you can use `str.removeprefix` and
/// `str.removesuffix` to remove an exact prefix or suffix from a string,
/// respectively, which should be preferred when possible.
///
/// ## Example

View File

@@ -137,13 +137,13 @@ fn is_standard_library_override(
return false;
};
match name {
// Ex) `Event#set`
// Ex) `Event.set`
"set" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter#filter`
// Ex) `Filter.filter`
"filter" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)

View File

@@ -7,17 +7,17 @@ use libcst_native::{
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
use ruff_python_ast::Expr;
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::Expr;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::edits::pad;
use crate::cst::helpers::space;
use crate::cst::helpers::{negate, space};
use crate::rules::flake8_comprehensions::rules::ObjectType;
use crate::{
checkers::ast::Checker,
@@ -718,7 +718,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
if outer_name.value == "list" {
tree = Expression::Call(Box::new((*inner_call).clone()));
} else {
// If the `reverse` argument is used
// If the `reverse` argument is used...
let args = if inner_call.args.iter().any(|arg| {
matches!(
arg.keyword,
@@ -728,7 +728,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
})
)
}) {
// Negate the `reverse` argument
// Negate the `reverse` argument.
inner_call
.args
.clone()
@@ -741,35 +741,35 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
..
})
) {
if let Expression::Name(ref val) = arg.value {
if val.value == "True" {
// TODO: even better would be to drop the argument, as False is the default
arg.value = Expression::Name(Box::new(Name {
value: "False",
lpar: vec![],
rpar: vec![],
}));
arg
} else if val.value == "False" {
arg.value = Expression::Name(Box::new(Name {
value: "True",
lpar: vec![],
rpar: vec![],
}));
arg
} else {
arg
}
} else {
arg
}
} else {
arg
arg.value = negate(&arg.value);
}
arg
})
.collect_vec()
} else {
let mut args = inner_call.args.clone();
// If necessary, parenthesize a generator expression, as a generator expression must
// be parenthesized if it's not a solitary argument. For example, given:
// ```python
// reversed(sorted(i for i in range(42)))
// ```
// Rewrite as:
// ```python
// sorted((i for i in range(42)), reverse=True)
// ```
if let [arg] = args.as_mut_slice() {
if matches!(arg.value, Expression::GeneratorExp(_)) {
if arg.value.lpar().is_empty() && arg.value.rpar().is_empty() {
arg.value = arg
.value
.clone()
.with_parens(LeftParen::default(), RightParen::default());
}
}
}
// Add the `reverse=True` argument.
args.push(Arg {
value: Expression::Name(Box::new(Name {
value: "True",

View File

@@ -1,5 +1,6 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, Comprehension, Expr};
use ruff_text_size::Ranged;
@@ -86,28 +87,16 @@ pub(crate) fn unnecessary_dict_comprehension(
if !generator.ifs.is_empty() || generator.is_async {
return;
}
let Some(key) = key.as_name_expr() else {
return;
};
let Some(value) = value.as_name_expr() else {
return;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &generator.target else {
return;
};
let [target_key, target_value] = elts.as_slice() else {
return;
};
let Some(target_key) = target_key.as_name_expr() else {
return;
};
let Some(target_value) = target_value.as_name_expr() else {
return;
};
if target_key.id != key.id {
if ComparableExpr::from(key) != ComparableExpr::from(target_key) {
return;
}
if target_value.id != value.id {
if ComparableExpr::from(value) != ComparableExpr::from(target_value) {
return;
}
add_diagnostic(checker, expr);
@@ -126,13 +115,7 @@ pub(crate) fn unnecessary_list_set_comprehension(
if !generator.ifs.is_empty() || generator.is_async {
return;
}
let Some(elt) = elt.as_name_expr() else {
return;
};
let Some(target) = generator.target.as_name_expr() else {
return;
};
if elt.id != target.id {
if ComparableExpr::from(elt) != ComparableExpr::from(&generator.target) {
return;
}
add_diagnostic(checker, expr);

View File

@@ -103,17 +103,18 @@ C413.py:7:1: C413 [*] Unnecessary `reversed` call around `sorted()`
7 |+sorted(x, key=lambda e: e, reverse=False)
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 9 | reversed(sorted(x, reverse=False))
10 10 |
10 10 | reversed(sorted(x, reverse=x))
C413.py:8:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
6 | reversed(sorted(x, reverse=True))
7 | reversed(sorted(x, key=lambda e: e, reverse=True))
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
9 | reversed(sorted(x, reverse=False))
|
= help: Remove unnecessary `reversed` call
|
6 | reversed(sorted(x, reverse=True))
7 | reversed(sorted(x, key=lambda e: e, reverse=True))
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
9 | reversed(sorted(x, reverse=False))
10 | reversed(sorted(x, reverse=x))
|
= help: Remove unnecessary `reversed` call
Suggested fix
5 5 | reversed(sorted(x, key=lambda e: e))
@@ -122,8 +123,8 @@ C413.py:8:1: C413 [*] Unnecessary `reversed` call around `sorted()`
8 |-reversed(sorted(x, reverse=True, key=lambda e: e))
8 |+sorted(x, reverse=False, key=lambda e: e)
9 9 | reversed(sorted(x, reverse=False))
10 10 |
11 11 | def reversed(*args, **kwargs):
10 10 | reversed(sorted(x, reverse=x))
11 11 | reversed(sorted(x, reverse=not x))
C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
@@ -131,8 +132,8 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 | reversed(sorted(x, reverse=False))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
10 |
11 | def reversed(*args, **kwargs):
10 | reversed(sorted(x, reverse=x))
11 | reversed(sorted(x, reverse=not x))
|
= help: Remove unnecessary `reversed` call
@@ -142,8 +143,87 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 |-reversed(sorted(x, reverse=False))
9 |+sorted(x, reverse=True)
10 10 |
11 11 | def reversed(*args, **kwargs):
12 12 | return None
10 10 | reversed(sorted(x, reverse=x))
11 11 | reversed(sorted(x, reverse=not x))
12 12 |
C413.py:10:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 | reversed(sorted(x, reverse=False))
10 | reversed(sorted(x, reverse=x))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
11 | reversed(sorted(x, reverse=not x))
|
= help: Remove unnecessary `reversed` call
Suggested fix
7 7 | reversed(sorted(x, key=lambda e: e, reverse=True))
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 9 | reversed(sorted(x, reverse=False))
10 |-reversed(sorted(x, reverse=x))
10 |+sorted(x, reverse=not x)
11 11 | reversed(sorted(x, reverse=not x))
12 12 |
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
C413.py:11:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
9 | reversed(sorted(x, reverse=False))
10 | reversed(sorted(x, reverse=x))
11 | reversed(sorted(x, reverse=not x))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
12 |
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
= help: Remove unnecessary `reversed` call
Suggested fix
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
9 9 | reversed(sorted(x, reverse=False))
10 10 | reversed(sorted(x, reverse=x))
11 |-reversed(sorted(x, reverse=not x))
11 |+sorted(x, reverse=x)
12 12 |
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
14 14 | reversed(sorted(i for i in range(42)))
C413.py:14:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
14 | reversed(sorted(i for i in range(42)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
15 | reversed(sorted((i for i in range(42)), reverse=True))
|
= help: Remove unnecessary `reversed` call
Suggested fix
11 11 | reversed(sorted(x, reverse=not x))
12 12 |
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
14 |-reversed(sorted(i for i in range(42)))
14 |+sorted((i for i in range(42)), reverse=True)
15 15 | reversed(sorted((i for i in range(42)), reverse=True))
16 16 |
17 17 |
C413.py:15:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
14 | reversed(sorted(i for i in range(42)))
15 | reversed(sorted((i for i in range(42)), reverse=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
= help: Remove unnecessary `reversed` call
Suggested fix
12 12 |
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
14 14 | reversed(sorted(i for i in range(42)))
15 |-reversed(sorted((i for i in range(42)), reverse=True))
15 |+sorted((i for i in range(42)), reverse=False)
16 16 |
17 17 |
18 18 | def reversed(*args, **kwargs):

View File

@@ -40,17 +40,18 @@ C416.py:7:1: C416 [*] Unnecessary `set` comprehension (rewrite using `set()`)
7 |+set(x)
8 8 | {k: v for k, v in y}
9 9 | {k: v for k, v in d.items()}
10 10 |
10 10 | [(k, v) for k, v in d.items()]
C416.py:8:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
|
6 | [i for i in x]
7 | {i for i in x}
8 | {k: v for k, v in y}
| ^^^^^^^^^^^^^^^^^^^^ C416
9 | {k: v for k, v in d.items()}
|
= help: Rewrite using `dict()`
|
6 | [i for i in x]
7 | {i for i in x}
8 | {k: v for k, v in y}
| ^^^^^^^^^^^^^^^^^^^^ C416
9 | {k: v for k, v in d.items()}
10 | [(k, v) for k, v in d.items()]
|
= help: Rewrite using `dict()`
Suggested fix
5 5 |
@@ -59,8 +60,8 @@ C416.py:8:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
8 |-{k: v for k, v in y}
8 |+dict(y)
9 9 | {k: v for k, v in d.items()}
10 10 |
11 11 | [i for i, in z]
10 10 | [(k, v) for k, v in d.items()]
11 11 | {k: (a, b) for k, (a, b) in d.items()}
C416.py:9:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
|
@@ -68,8 +69,8 @@ C416.py:9:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
8 | {k: v for k, v in y}
9 | {k: v for k, v in d.items()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
10 |
11 | [i for i, in z]
10 | [(k, v) for k, v in d.items()]
11 | {k: (a, b) for k, (a, b) in d.items()}
|
= help: Rewrite using `dict()`
@@ -79,23 +80,64 @@ C416.py:9:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
8 8 | {k: v for k, v in y}
9 |-{k: v for k, v in d.items()}
9 |+dict(d.items())
10 10 |
11 11 | [i for i, in z]
12 12 | [i for i, j in y]
10 10 | [(k, v) for k, v in d.items()]
11 11 | {k: (a, b) for k, (a, b) in d.items()}
12 12 |
C416.py:22:70: C416 [*] Unnecessary `list` comprehension (rewrite using `list()`)
C416.py:10:1: C416 [*] Unnecessary `list` comprehension (rewrite using `list()`)
|
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
22 | any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
8 | {k: v for k, v in y}
9 | {k: v for k, v in d.items()}
10 | [(k, v) for k, v in d.items()]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
11 | {k: (a, b) for k, (a, b) in d.items()}
|
= help: Rewrite using `list()`
Suggested fix
7 7 | {i for i in x}
8 8 | {k: v for k, v in y}
9 9 | {k: v for k, v in d.items()}
10 |-[(k, v) for k, v in d.items()]
10 |+list(d.items())
11 11 | {k: (a, b) for k, (a, b) in d.items()}
12 12 |
13 13 | [i for i, in z]
C416.py:11:1: C416 [*] Unnecessary `dict` comprehension (rewrite using `dict()`)
|
9 | {k: v for k, v in d.items()}
10 | [(k, v) for k, v in d.items()]
11 | {k: (a, b) for k, (a, b) in d.items()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
12 |
13 | [i for i, in z]
|
= help: Rewrite using `dict()`
Suggested fix
8 8 | {k: v for k, v in y}
9 9 | {k: v for k, v in d.items()}
10 10 | [(k, v) for k, v in d.items()]
11 |-{k: (a, b) for k, (a, b) in d.items()}
11 |+dict(d.items())
12 12 |
13 13 | [i for i, in z]
14 14 | [i for i, j in y]
C416.py:24:70: C416 [*] Unnecessary `list` comprehension (rewrite using `list()`)
|
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
24 | any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
| ^^^^^^^^^^^^^^^^^^^^^^^ C416
|
= help: Rewrite using `list()`
Suggested fix
19 19 | {k: v if v else None for k, v in y}
20 20 |
21 21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
22 |-any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
22 |+any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in list(SymbolType))
21 21 | {k: v if v else None for k, v in y}
22 22 |
23 23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
24 |-any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
24 |+any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in list(SymbolType))

View File

@@ -0,0 +1,27 @@
//! Rules from [flake8-logging](https://pypi.org/project/flake8-logging/).
pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::DirectLoggerInstantiation, Path::new("LOG001.py"))]
#[test_case(Rule::UndocumentedWarn, Path::new("LOG009.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_logging").join(path).as_path(),
&Settings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,77 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
/// ## What it does
/// Checks for direct instantiation of `logging.Logger`, as opposed to using
/// `logging.getLogger()`.
///
/// ## Why is this bad?
/// The [Logger Objects] documentation states that:
///
/// > Note that Loggers should NEVER be instantiated directly, but always
/// > through the module-level function `logging.getLogger(name)`.
///
/// If a logger is directly instantiated, it won't be added to the logger
/// tree, and will bypass all configuration. Messages logged to it will
/// only be sent to the "handler of last resort", skipping any filtering
/// or formatting.
///
/// ## Example
/// ```python
/// import logging
///
/// logger = logging.Logger(__name__)
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
/// logger = logging.getLogger(__name__)
/// ```
///
/// [Logger Objects]: https://docs.python.org/3/library/logging.html#logger-objects
#[violation]
pub struct DirectLoggerInstantiation;
impl Violation for DirectLoggerInstantiation {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `logging.getLogger()` to instantiate loggers")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `logging.getLogger()`"))
}
}
/// LOG001
pub(crate) fn direct_logger_instantiation(checker: &mut Checker, call: &ast::ExprCall) {
if checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Logger"]))
{
let mut diagnostic = Diagnostic::new(DirectLoggerInstantiation, call.func.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("logging", "getLogger"),
call.func.start(),
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, call.func.range());
Ok(Fix::suggested_edits(import_edit, [reference_edit]))
});
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -0,0 +1,5 @@
pub(crate) use direct_logger_instantiation::*;
pub(crate) use undocumented_warn::*;
mod direct_logger_instantiation;
mod undocumented_warn;

View File

@@ -0,0 +1,71 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `logging.WARN`.
///
/// ## Why is this bad?
/// The `logging.WARN` constant is an undocumented alias for `logging.WARNING`.
///
/// Although its not explicitly deprecated, `logging.WARN` is not mentioned
/// in the `logging` documentation. Prefer `logging.WARNING` instead.
///
/// ## Example
/// ```python
/// import logging
///
///
/// logging.basicConfig(level=logging.WARN)
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
///
/// logging.basicConfig(level=logging.WARNING)
/// ```
#[violation]
pub struct UndocumentedWarn;
impl Violation for UndocumentedWarn {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of undocumented `logging.WARN` constant")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace `logging.WARN` with `logging.WARNING`"))
}
}
/// LOG009
pub(crate) fn undocumented_warn(checker: &mut Checker, expr: &Expr) {
if checker
.semantic()
.resolve_call_path(expr)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "WARN"]))
{
let mut diagnostic = Diagnostic::new(UndocumentedWarn, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("logging", "WARNING"),
expr.start(),
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, expr.range());
Ok(Fix::suggested_edits(import_edit, [reference_edit]))
});
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -0,0 +1,40 @@
---
source: crates/ruff/src/rules/flake8_logging/mod.rs
---
LOG001.py:3:1: LOG001 [*] Use `logging.getLogger()` to instantiate loggers
|
1 | import logging
2 |
3 | logging.Logger(__name__)
| ^^^^^^^^^^^^^^ LOG001
4 | logging.Logger()
5 | logging.getLogger(__name__)
|
= help: Replace with `logging.getLogger()`
Suggested fix
1 1 | import logging
2 2 |
3 |-logging.Logger(__name__)
3 |+logging.getLogger(__name__)
4 4 | logging.Logger()
5 5 | logging.getLogger(__name__)
LOG001.py:4:1: LOG001 [*] Use `logging.getLogger()` to instantiate loggers
|
3 | logging.Logger(__name__)
4 | logging.Logger()
| ^^^^^^^^^^^^^^ LOG001
5 | logging.getLogger(__name__)
|
= help: Replace with `logging.getLogger()`
Suggested fix
1 1 | import logging
2 2 |
3 3 | logging.Logger(__name__)
4 |-logging.Logger()
4 |+logging.getLogger()
5 5 | logging.getLogger(__name__)

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff/src/rules/flake8_logging/mod.rs
---
LOG009.py:3:1: LOG009 [*] Use of undocumented `logging.WARN` constant
|
1 | import logging
2 |
3 | logging.WARN # LOG009
| ^^^^^^^^^^^^ LOG009
4 | logging.WARNING # OK
|
= help: Replace `logging.WARN` with `logging.WARNING`
Suggested fix
1 1 | import logging
2 2 |
3 |-logging.WARN # LOG009
3 |+logging.WARNING # LOG009
4 4 | logging.WARNING # OK
5 5 |
6 6 | from logging import WARN, WARNING
LOG009.py:8:1: LOG009 [*] Use of undocumented `logging.WARN` constant
|
6 | from logging import WARN, WARNING
7 |
8 | WARN # LOG009
| ^^^^ LOG009
9 | WARNING # OK
|
= help: Replace `logging.WARN` with `logging.WARNING`
Suggested fix
5 5 |
6 6 | from logging import WARN, WARNING
7 7 |
8 |-WARN # LOG009
8 |+logging.WARNING # LOG009
9 9 | WARNING # OK

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use anyhow::{bail, Context};
use libcst_native::{
self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizedNode, SimpleStatementLine,
SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOperation,
SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace,
};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -21,7 +21,7 @@ use ruff_text_size::Ranged;
use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker;
use crate::cst::helpers::space;
use crate::cst::helpers::negate;
use crate::cst::matchers::match_indented_block;
use crate::cst::matchers::match_module;
use crate::importer::ImportRequest;
@@ -567,23 +567,6 @@ fn is_composite_condition(test: &Expr) -> CompositionKind {
CompositionKind::None
}
/// Negate a condition, i.e., `a` => `not a` and `not a` => `a`.
fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
if let Expression::UnaryOperation(ref expression) = expression {
if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) {
return *expression.expression.clone();
}
}
Expression::UnaryOperation(Box::new(UnaryOperation {
operator: libcst_native::UnaryOp::Not {
whitespace_after: space(),
},
expression: Box::new(expression.clone()),
lpar: vec![],
rpar: vec![],
}))
}
/// Propagate parentheses from a parent to a child expression, if necessary.
///
/// For example, when splitting:

View File

@@ -12,10 +12,10 @@ use crate::registry::AsRule;
use crate::rules::flynt::helpers;
/// ## What it does
/// Checks for `str#join` calls that can be replaced with f-strings.
/// Checks for `str.join` calls that can be replaced with f-strings.
///
/// ## Why is this bad?
/// f-strings are more readable and generally preferred over `str#join` calls.
/// f-strings are more readable and generally preferred over `str.join` calls.
///
/// ## Example
/// ```python

View File

@@ -22,6 +22,7 @@ pub mod flake8_future_annotations;
pub mod flake8_gettext;
pub mod flake8_implicit_str_concat;
pub mod flake8_import_conventions;
pub mod flake8_logging;
pub mod flake8_logging_format;
pub mod flake8_no_pep420;
pub mod flake8_pie;

View File

@@ -19,6 +19,7 @@ mod tests {
#[test_case(Rule::TryExceptInLoop, Path::new("PERF203.py"))]
#[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))]
#[test_case(Rule::ManualListCopy, Path::new("PERF402.py"))]
#[test_case(Rule::ManualDictComprehension, Path::new("PERF403.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,178 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::analyze::typing::is_dict;
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `for` loops that can be replaced by a dictionary comprehension.
///
/// ## Why is this bad?
/// When creating or extending a dictionary in a for-loop, prefer a dictionary
/// comprehension. Comprehensions are more readable and more performant.
///
/// For example, when comparing `{x: x for x in list(range(1000))}` to the `for`
/// loop version, the comprehension is ~10% faster on Python 3.11.
///
/// Note that, as with all `perflint` rules, this is only intended as a
/// micro-optimization, and will have a negligible impact on performance in
/// most cases.
///
/// ## Example
/// ```python
/// pairs = (("a", 1), ("b", 2))
/// result = {}
/// for x, y in pairs:
/// if y % 2:
/// result[x] = y
/// ```
///
/// Use instead:
/// ```python
/// pairs = (("a", 1), ("b", 2))
/// result = {x: y for x, y in pairs if y % 2}
/// ```
///
/// If you're appending to an existing dictionary, use the `update` method instead:
/// ```python
/// pairs = (("a", 1), ("b", 2))
/// result.update({x: y for x, y in pairs if y % 2})
/// ```
#[violation]
pub struct ManualDictComprehension;
impl Violation for ManualDictComprehension {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use a dictionary comprehension instead of a for-loop")
}
}
/// PERF403
pub(crate) fn manual_dict_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
let (stmt, if_test) = match body {
// ```python
// for idx, name in enumerate(names):
// if idx % 2 == 0:
// result[name] = idx
// ```
[Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
test,
..
})] => {
// TODO(charlie): If there's an `else` clause, verify that the `else` has the
// same structure.
if !elif_else_clauses.is_empty() {
return;
}
let [stmt] = body.as_slice() else {
return;
};
(stmt, Some(test))
}
// ```python
// for idx, name in enumerate(names):
// result[name] = idx
// ```
[stmt] => (stmt, None),
_ => return,
};
let Stmt::Assign(ast::StmtAssign {
targets,
value,
range,
}) = stmt
else {
return;
};
let [Expr::Subscript(ast::ExprSubscript {
value: subscript_value,
slice,
..
})] = targets.as_slice()
else {
return;
};
match target {
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
if !elts
.iter()
.any(|elt| ComparableExpr::from(slice) == ComparableExpr::from(elt))
{
return;
}
if !elts
.iter()
.any(|elt| ComparableExpr::from(value) == ComparableExpr::from(elt))
{
return;
}
}
Expr::Name(_) => {
if ComparableExpr::from(slice) != ComparableExpr::from(target) {
return;
}
if ComparableExpr::from(value) != ComparableExpr::from(target) {
return;
}
}
_ => return,
}
// Exclude non-dictionary value.
let Expr::Name(ast::ExprName {
id: subscript_name, ..
}) = subscript_value.as_ref()
else {
return;
};
let scope = checker.semantic().current_scope();
let bindings: Vec<&Binding> = scope
.get_all(subscript_name)
.map(|binding_id| checker.semantic().binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
// Avoid if the value is used in the conditional test, e.g.,
//
// ```python
// for x in y:
// if x in filtered:
// filtered[x] = y
// ```
//
// Converting this to a dictionary comprehension would raise a `NameError` as
// `filtered` is not defined yet:
//
// ```python
// filtered = {x: y for x in y if x in filtered}
// ```
if if_test.is_some_and(|test| {
any_over_expr(test, &|expr| {
expr.as_name_expr()
.is_some_and(|expr| expr.id == *subscript_name)
})
}) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(ManualDictComprehension, *range));
}

View File

@@ -1,10 +1,12 @@
pub(crate) use incorrect_dict_iterator::*;
pub(crate) use manual_dict_comprehension::*;
pub(crate) use manual_list_comprehension::*;
pub(crate) use manual_list_copy::*;
pub(crate) use try_except_in_loop::*;
pub(crate) use unnecessary_list_cast::*;
mod incorrect_dict_iterator;
mod manual_dict_comprehension;
mod manual_list_comprehension;
mod manual_list_copy;
mod try_except_in_loop;

View File

@@ -0,0 +1,52 @@
---
source: crates/ruff/src/rules/perflint/mod.rs
---
PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
3 | result = {}
4 | for idx, name in enumerate(fruit):
5 | result[idx] = name # PERF403
| ^^^^^^^^^^^^^^^^^^ PERF403
|
PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
11 | for idx, name in enumerate(fruit):
12 | if idx % 2:
13 | result[idx] = name # PERF403
| ^^^^^^^^^^^^^^^^^^ PERF403
|
PERF403.py:31:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
29 | for idx, name in enumerate(fruit):
30 | if idx % 2:
31 | result[idx] = name # PERF403
| ^^^^^^^^^^^^^^^^^^ PERF403
|
PERF403.py:61:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
59 | for idx, name in enumerate(fruit):
60 | if idx % 2:
61 | result[idx] = name # PERF403
| ^^^^^^^^^^^^^^^^^^ PERF403
|
PERF403.py:76:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
74 | result = {}
75 | for name in fruit:
76 | result[name] = name # PERF403
| ^^^^^^^^^^^^^^^^^^^ PERF403
|
PERF403.py:83:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
81 | result = {}
82 | for idx, name in enumerate(fruit):
83 | result[name] = idx # PERF403
| ^^^^^^^^^^^^^^^^^^ PERF403
|

View File

@@ -70,6 +70,10 @@ impl Violation for BlankLineAfterSummary {
pub(crate) fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) {
let body = docstring.body();
if !docstring.triple_quoted() {
return;
}
let mut lines_count: usize = 1;
let mut blanks_count = 0;
for line in body.trim().universal_newlines().skip(1) {

View File

@@ -63,6 +63,10 @@ pub(crate) fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Do
let contents = docstring.contents;
let body = docstring.body();
if !docstring.triple_quoted() {
return;
}
let mut line_count = 0;
for line in NewlineWithTrailingNewline::from(body.as_str()) {
if !line.trim().is_empty() {

View File

@@ -1691,7 +1691,10 @@ fn common_section(
}
if checker.enabled(Rule::NoBlankLineBeforeSection) {
if !context.previous_line().is_some_and(str::is_empty) {
if !context
.previous_line()
.is_some_and(|line| line.trim().is_empty())
{
let mut diagnostic = Diagnostic::new(
NoBlankLineBeforeSection {
name: context.section_name().to_string(),

View File

@@ -97,5 +97,6 @@ D.py:658:5: D204 [*] 1 blank line required after class docstring
659 |+
659 660 | def sort_services(self):
660 661 | pass
661 662 |

View File

@@ -77,4 +77,13 @@ D.py:658:5: D300 Use triple double quotes `"""`
660 | pass
|
D.py:664:5: D300 Use triple double quotes `"""`
|
663 | def newline_after_closing_quote(self):
664 | "We enforce a newline after the closing quote for a multi-line docstring \
| _____^
665 | | but continuations shouldn't be considered multi-line"
| |_________________________________________________________^ D300
|

View File

@@ -310,4 +310,21 @@ D.py:641:18: D400 [*] First line should end with a period
643 643 |
644 644 | def single_line_docstring_with_an_escaped_backslash():
D.py:664:5: D400 [*] First line should end with a period
|
663 | def newline_after_closing_quote(self):
664 | "We enforce a newline after the closing quote for a multi-line docstring \
| _____^
665 | | but continuations shouldn't be considered multi-line"
| |_________________________________________________________^ D400
|
= help: Add period
Suggested fix
662 662 |
663 663 | def newline_after_closing_quote(self):
664 664 | "We enforce a newline after the closing quote for a multi-line docstring \
665 |- but continuations shouldn't be considered multi-line"
665 |+ but continuations shouldn't be considered multi-line."

View File

@@ -496,5 +496,6 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
530 |+ ----------
530 531 | ===========
531 532 | """
532 533 |

View File

@@ -292,4 +292,21 @@ D.py:641:18: D415 [*] First line should end with a period, question mark, or exc
643 643 |
644 644 | def single_line_docstring_with_an_escaped_backslash():
D.py:664:5: D415 [*] First line should end with a period, question mark, or exclamation point
|
663 | def newline_after_closing_quote(self):
664 | "We enforce a newline after the closing quote for a multi-line docstring \
| _____^
665 | | but continuations shouldn't be considered multi-line"
| |_________________________________________________________^ D415
|
= help: Add closing punctuation
Suggested fix
662 662 |
663 663 | def newline_after_closing_quote(self):
664 664 | "We enforce a newline after the closing quote for a multi-line docstring \
665 |- but continuations shouldn't be considered multi-line"
665 |+ but continuations shouldn't be considered multi-line."

View File

@@ -256,4 +256,20 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn too_many_public_methods() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/too_many_public_methods.py"),
&Settings {
pylint: pylint::settings::Settings {
max_public_methods: 7,
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::TooManyPublicMethods])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -11,15 +11,15 @@ use crate::checkers::ast::Checker;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks duplicate characters in `str#strip` calls.
/// Checks duplicate characters in `str.strip` calls.
///
/// ## Why is this bad?
/// All characters in `str#strip` calls are removed from both the leading and
/// All characters in `str.strip` calls are removed from both the leading and
/// trailing ends of the string. Including duplicate characters in the call
/// is redundant and often indicative of a mistake.
///
/// In Python 3.9 and later, you can use `str#removeprefix` and
/// `str#removesuffix` to remove an exact prefix or suffix from a string,
/// In Python 3.9 and later, you can use `str.removeprefix` and
/// `str.removesuffix` to remove an exact prefix or suffix from a string,
/// respectively, which should be preferred when possible.
///
/// ## Example

View File

@@ -43,6 +43,7 @@ pub(crate) use subprocess_run_without_check::*;
pub(crate) use sys_exit_alias::*;
pub(crate) use too_many_arguments::*;
pub(crate) use too_many_branches::*;
pub(crate) use too_many_public_methods::*;
pub(crate) use too_many_return_statements::*;
pub(crate) use too_many_statements::*;
pub(crate) use type_bivariance::*;
@@ -101,6 +102,7 @@ mod subprocess_run_without_check;
mod sys_exit_alias;
mod too_many_arguments;
mod too_many_branches;
mod too_many_public_methods;
mod too_many_return_statements;
mod too_many_statements;
mod type_bivariance;

View File

@@ -10,7 +10,7 @@ use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::hashable::HashableExpr;
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextSize};
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
@@ -74,7 +74,8 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
return;
}
let mut value_to_comparators: FxHashMap<HashableExpr, (usize, Vec<&Expr>)> =
// Map from expression hash to (starting offset, number of comparisons, list
let mut value_to_comparators: FxHashMap<HashableExpr, (TextSize, Vec<&Expr>)> =
FxHashMap::with_capacity_and_hasher(
bool_op.values.len() * 2,
BuildHasherDefault::default(),
@@ -95,30 +96,31 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
};
if matches!(left.as_ref(), Expr::Name(_) | Expr::Attribute(_)) {
let (left_count, left_matches) = value_to_comparators
let (_, left_matches) = value_to_comparators
.entry(left.deref().into())
.or_insert_with(|| (0, Vec::new()));
*left_count += 1;
.or_insert_with(|| (left.start(), Vec::new()));
left_matches.push(right);
}
if matches!(right, Expr::Name(_) | Expr::Attribute(_)) {
let (right_count, right_matches) = value_to_comparators
let (_, right_matches) = value_to_comparators
.entry(right.into())
.or_insert_with(|| (0, Vec::new()));
*right_count += 1;
.or_insert_with(|| (right.start(), Vec::new()));
right_matches.push(left);
}
}
for (value, (count, comparators)) in value_to_comparators {
if count > 1 {
for (value, (_, comparators)) in value_to_comparators
.iter()
.sorted_by_key(|(_, (start, _))| *start)
{
if comparators.len() > 1 {
checker.diagnostics.push(Diagnostic::new(
RepeatedEqualityComparison {
expression: SourceCodeSnippet::new(merged_membership_test(
value.as_expr(),
bool_op.op,
&comparators,
comparators,
checker.locator(),
)),
},

View File

@@ -0,0 +1,126 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::analyze::visibility::{self, Visibility::Public};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for classes with too many public methods
///
/// By default, this rule allows up to 20 statements, as configured by the
/// [`pylint.max-public-methods`] option.
///
/// ## Why is this bad?
/// Classes with many public methods are harder to understand
/// and maintain.
///
/// Instead, consider refactoring the class into separate classes.
///
/// ## Example
/// Assuming that `pylint.max-public-settings` is set to 5:
/// ```python
/// class Linter:
/// def __init__(self):
/// pass
///
/// def pylint(self):
/// pass
///
/// def pylint_settings(self):
/// pass
///
/// def flake8(self):
/// pass
///
/// def flake8_settings(self):
/// pass
///
/// def pydocstyle(self):
/// pass
///
/// def pydocstyle_settings(self):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class Linter:
/// def __init__(self):
/// self.pylint = Pylint()
/// self.flake8 = Flake8()
/// self.pydocstyle = Pydocstyle()
///
/// def lint(self):
/// pass
///
///
/// class Pylint:
/// def lint(self):
/// pass
///
/// def settings(self):
/// pass
///
///
/// class Flake8:
/// def lint(self):
/// pass
///
/// def settings(self):
/// pass
///
///
/// class Pydocstyle:
/// def lint(self):
/// pass
///
/// def settings(self):
/// pass
/// ```
///
/// ## Options
/// - `pylint.max-public-methods`
#[violation]
pub struct TooManyPublicMethods {
methods: usize,
max_methods: usize,
}
impl Violation for TooManyPublicMethods {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyPublicMethods {
methods,
max_methods,
} = self;
format!("Too many public methods ({methods} > {max_methods})")
}
}
/// R0904
pub(crate) fn too_many_public_methods(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
max_methods: usize,
) {
let methods = class_def
.body
.iter()
.filter(|stmt| {
stmt.as_function_def_stmt()
.is_some_and(|node| matches!(visibility::method_visibility(node), Public))
})
.count();
if methods > max_methods {
checker.diagnostics.push(Diagnostic::new(
TooManyPublicMethods {
methods,
max_methods,
},
class_def.range(),
));
}
}

View File

@@ -42,6 +42,7 @@ pub struct Settings {
pub max_returns: usize,
pub max_branches: usize,
pub max_statements: usize,
pub max_public_methods: usize,
}
impl Default for Settings {
@@ -52,6 +53,7 @@ impl Default for Settings {
max_returns: 6,
max_branches: 12,
max_statements: 50,
max_public_methods: 20,
}
}
}

View File

@@ -0,0 +1,46 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
too_many_public_methods.py:1:1: PLR0904 Too many public methods (10 > 7)
|
1 | / class Everything:
2 | | foo = 1
3 | |
4 | | def __init__(self):
5 | | pass
6 | |
7 | | def _private(self):
8 | | pass
9 | |
10 | | def method1(self):
11 | | pass
12 | |
13 | | def method2(self):
14 | | pass
15 | |
16 | | def method3(self):
17 | | pass
18 | |
19 | | def method4(self):
20 | | pass
21 | |
22 | | def method5(self):
23 | | pass
24 | |
25 | | def method6(self):
26 | | pass
27 | |
28 | | def method7(self):
29 | | pass
30 | |
31 | | def method8(self):
32 | | pass
33 | |
34 | | def method9(self):
35 | | pass
| |____________^ PLR0904
36 |
37 | class Small:
|

View File

@@ -138,16 +138,52 @@ const PIPES_TO_SHLEX: &[&str] = &["quote"];
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[
"AbstractSet",
"AnyStr",
"AsyncIterable",
"AsyncIterator",
"Awaitable",
"BinaryIO",
"Callable",
"ClassVar",
"Collection",
"Container",
"ContextManager",
"Coroutine",
"DefaultDict",
"Dict",
"FrozenSet",
"Generator",
"Generic",
"Hashable",
"IO",
"ItemsView",
"Iterable",
"Iterator",
"KeysView",
"List",
"Mapping",
"MappingView",
"Match",
"MutableMapping",
"MutableSequence",
"MutableSet",
"Optional",
"Pattern",
"Reversible",
"Sequence",
"Set",
"Sized",
"TYPE_CHECKING",
"Text",
"TextIO",
"Tuple",
"Type",
"Union",
"ValuesView",
"cast",
"no_type_check",
"no_type_check_decorator",
// Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and
// optimizations,
// "NewType",
@@ -165,6 +201,7 @@ const TYPING_EXTENSIONS_TO_TYPING_37: &[&str] = &[
"ChainMap",
"Counter",
"Deque",
"ForwardRef",
"NoReturn",
];
@@ -287,6 +324,18 @@ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
"NamedTuple",
// Introduced in Python 3.8, but `typing_extensions` backports a ton of optimizations that were
// added in Python 3.12.
"Protocol",
"SupportsAbs",
"SupportsBytes",
"SupportsComplex",
"SupportsFloat",
"SupportsInt",
"SupportsRound",
"TypedDict",
"Unpack",
// Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument,
// which was introduced in Python 3.12.
"dataclass_transform",

View File

@@ -22,10 +22,10 @@ use crate::rules::pyflakes::format::FormatSummary;
use crate::rules::pyupgrade::helpers::curly_escape;
/// ## What it does
/// Checks for `str#format` calls that can be replaced with f-strings.
/// Checks for `str.format` calls that can be replaced with f-strings.
///
/// ## Why is this bad?
/// f-strings are more readable and generally preferred over `str#format`
/// f-strings are more readable and generally preferred over `str.format`
/// calls.
///
/// ## Example

View File

@@ -42,13 +42,16 @@ pub struct UnnecessaryEncodeUTF8 {
impl AlwaysAutofixableViolation for UnnecessaryEncodeUTF8 {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary call to `encode` as UTF-8")
match self.reason {
Reason::BytesLiteral => format!("Unnecessary call to `encode` as UTF-8"),
Reason::DefaultArgument => format!("Unnecessary UTF-8 `encoding` argument to `encode`"),
}
}
fn autofix_title(&self) -> String {
match self.reason {
Reason::BytesLiteral => "Rewrite as bytes literal".to_string(),
Reason::DefaultArgument => "Remove unnecessary encoding argument".to_string(),
Reason::DefaultArgument => "Remove unnecessary `encoding` argument".to_string(),
}
}
}

View File

@@ -1,4 +1,6 @@
use ast::{Constant, ExprCall, ExprConstant};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{
self as ast,
visitor::{self, Visitor},
@@ -6,20 +8,29 @@ use ruff_python_ast::{
TypeParam, TypeParamTypeVar,
};
use ruff_python_semantic::SemanticModel;
use crate::{registry::AsRule, settings::types::PythonVersion};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::{registry::AsRule, settings::types::PythonVersion};
/// ## What it does
/// Checks for use of `TypeAlias` annotation for declaring type aliases.
///
/// ## Why is this bad?
/// The `type` keyword was introduced in Python 3.12 by PEP-695 for defining type aliases.
/// The type keyword is easier to read and provides cleaner support for generics.
/// The `type` keyword was introduced in Python 3.12 by [PEP 695] for defining
/// type aliases. The `type` keyword is easier to read and provides cleaner
/// support for generics.
///
/// ## Known problems
/// [PEP 695] uses inferred variance for type parameters, instead of the
/// `covariant` and `contravariant` keywords used by `TypeParam` variables. As
/// such, rewriting a `TypeParam` variable to a `type` alias may change its
/// variance.
///
/// Unlike `TypeParam` variables, [PEP 695]-style `type` aliases cannot be used
/// at runtime. For example, calling `isinstance` on a `type` alias will throw
/// a `TypeError`. As such, rewriting a `TypeParam` via the `type` keyword will
/// cause issues for parameters that are used for such runtime checks.
///
/// ## Example
/// ```python
@@ -30,6 +41,8 @@ use crate::checkers::ast::Checker;
/// ```python
/// type ListOfInt = list[int]
/// ```
///
/// [PEP 695]: https://peps.python.org/pep-0695/
#[violation]
pub struct NonPEP695TypeAlias {
name: String,
@@ -83,24 +96,36 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
let mut diagnostic = Diagnostic::new(NonPEP695TypeAlias { name: name.clone() }, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let mut visitor = TypeVarReferenceVisitor {
names: vec![],
vars: vec![],
semantic: checker.semantic(),
};
visitor.visit_expr(value);
let type_params = if visitor.names.is_empty() {
let type_params = if visitor.vars.is_empty() {
None
} else {
Some(ast::TypeParams {
range: TextRange::default(),
type_params: visitor
.names
.iter()
.map(|name| {
.vars
.into_iter()
.map(|TypeVar { name, restriction }| {
TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
name: Identifier::new(name.id.clone(), TextRange::default()),
bound: None,
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => {
Some(Box::new(bound.clone()))
}
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
elts: constraints.into_iter().cloned().collect(),
ctx: ast::ExprContext::Load,
})))
}
None => None,
},
})
})
.collect(),
@@ -120,8 +145,22 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
checker.diagnostics.push(diagnostic);
}
#[derive(Debug)]
enum TypeVarRestriction<'a> {
/// A type variable with a bound, e.g., `TypeVar("T", bound=int)`.
Bound(&'a Expr),
/// A type variable with constraints, e.g., `TypeVar("T", int, str)`.
Constraint(Vec<&'a Expr>),
}
#[derive(Debug)]
struct TypeVar<'a> {
name: &'a ExprName,
restriction: Option<TypeVarRestriction<'a>>,
}
struct TypeVarReferenceVisitor<'a> {
names: Vec<&'a ExprName>,
vars: Vec<TypeVar<'a>>,
semantic: &'a SemanticModel<'a>,
}
@@ -149,16 +188,16 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
..
}) => {
if self.semantic.match_typing_expr(subscript_value, "TypeVar") {
self.names.push(name);
self.vars.push(TypeVar {
name,
restriction: None,
});
}
}
Expr::Call(ExprCall {
func, arguments, ..
}) => {
// TODO(zanieb): Add support for bounds and variance declarations
// for now this only supports `TypeVar("...")`
if self.semantic.match_typing_expr(func, "TypeVar")
&& arguments.args.len() == 1
&& arguments.args.first().is_some_and(|arg| {
matches!(
arg,
@@ -168,9 +207,18 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
})
)
})
&& arguments.keywords.is_empty()
{
self.names.push(name);
let restriction = if let Some(bound) = arguments.find_keyword("bound") {
Some(TypeVarRestriction::Bound(&bound.value))
} else if arguments.args.len() > 1 {
Some(TypeVarRestriction::Constraint(
arguments.args.iter().skip(1).collect(),
))
} else {
None
};
self.vars.push(TypeVar { name, restriction });
}
}
_ => {}

View File

@@ -227,7 +227,7 @@ UP012.py:24:5: UP012 [*] Unnecessary call to `encode` as UTF-8
26 26 |
27 27 | # `encode` on variables should not be processed.
UP012.py:32:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:32:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
31 | bar = "bar"
32 | f"foo{bar}".encode("utf-8")
@@ -235,7 +235,7 @@ UP012.py:32:1: UP012 [*] Unnecessary call to `encode` as UTF-8
33 | encoding = "latin"
34 | "foo".encode(encoding)
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
29 29 | string.encode("utf-8")
@@ -247,7 +247,7 @@ UP012.py:32:1: UP012 [*] Unnecessary call to `encode` as UTF-8
34 34 | "foo".encode(encoding)
35 35 | f"foo{bar}".encode(encoding)
UP012.py:36:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:36:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
34 | "foo".encode(encoding)
35 | f"foo{bar}".encode(encoding)
@@ -258,7 +258,7 @@ UP012.py:36:1: UP012 [*] Unnecessary call to `encode` as UTF-8
39 |
40 | # `encode` with custom args and kwargs should not be processed.
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
33 33 | encoding = "latin"
@@ -272,7 +272,7 @@ UP012.py:36:1: UP012 [*] Unnecessary call to `encode` as UTF-8
40 38 | # `encode` with custom args and kwargs should not be processed.
41 39 | "foo".encode("utf-8", errors="replace")
UP012.py:53:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:53:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
52 | # Unicode literals should only be stripped of default encoding.
53 | "unicode text©".encode("utf-8") # "unicode text©".encode()
@@ -280,7 +280,7 @@ UP012.py:53:1: UP012 [*] Unnecessary call to `encode` as UTF-8
54 | "unicode text©".encode()
55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode()
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
50 50 | "unicode text©".encode(encoding="utf-8", errors="replace")
@@ -292,7 +292,7 @@ UP012.py:53:1: UP012 [*] Unnecessary call to `encode` as UTF-8
55 55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode()
56 56 |
UP012.py:55:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:55:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
53 | "unicode text©".encode("utf-8") # "unicode text©".encode()
54 | "unicode text©".encode()
@@ -301,7 +301,7 @@ UP012.py:55:1: UP012 [*] Unnecessary call to `encode` as UTF-8
56 |
57 | r"foo\o".encode("utf-8") # br"foo\o"
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
52 52 | # Unicode literals should only be stripped of default encoding.
@@ -471,7 +471,7 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8
74 74 | (f"foo{bar}").encode("utf-8")
75 75 | (f"foo{bar}").encode(encoding="utf-8")
UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:74:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
72 | )).encode()
73 |
@@ -480,7 +480,7 @@ UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8
75 | (f"foo{bar}").encode(encoding="utf-8")
76 | ("unicode text©").encode("utf-8")
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
71 71 | "def"
@@ -492,7 +492,7 @@ UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8
76 76 | ("unicode text©").encode("utf-8")
77 77 | ("unicode text©").encode(encoding="utf-8")
UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:75:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
74 | (f"foo{bar}").encode("utf-8")
75 | (f"foo{bar}").encode(encoding="utf-8")
@@ -500,7 +500,7 @@ UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8
76 | ("unicode text©").encode("utf-8")
77 | ("unicode text©").encode(encoding="utf-8")
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
72 72 | )).encode()
@@ -511,7 +511,7 @@ UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8
76 76 | ("unicode text©").encode("utf-8")
77 77 | ("unicode text©").encode(encoding="utf-8")
UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:76:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
74 | (f"foo{bar}").encode("utf-8")
75 | (f"foo{bar}").encode(encoding="utf-8")
@@ -519,7 +519,7 @@ UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012
77 | ("unicode text©").encode(encoding="utf-8")
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
73 73 |
@@ -529,14 +529,14 @@ UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8
76 |+("unicode text©").encode()
77 77 | ("unicode text©").encode(encoding="utf-8")
UP012.py:77:1: UP012 [*] Unnecessary call to `encode` as UTF-8
UP012.py:77:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
75 | (f"foo{bar}").encode(encoding="utf-8")
76 | ("unicode text©").encode("utf-8")
77 | ("unicode text©").encode(encoding="utf-8")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012
|
= help: Remove unnecessary encoding argument
= help: Remove unnecessary `encoding` argument
Fix
74 74 | (f"foo{bar}").encode("utf-8")

View File

@@ -931,7 +931,7 @@ UP035.py:74:1: UP035 [*] Import from `collections.abc` instead: `AsyncGenerator`
74 |+from collections.abc import AsyncGenerator
75 75 | from typing import Reversible
76 76 | from typing import Generator
77 77 |
77 77 | from typing import Callable
UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
|
@@ -940,6 +940,7 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
75 | from typing import Reversible
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
76 | from typing import Generator
77 | from typing import Callable
|
= help: Import from `collections.abc`
@@ -950,8 +951,8 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
75 |-from typing import Reversible
75 |+from collections.abc import Reversible
76 76 | from typing import Generator
77 77 |
78 78 | # OK
77 77 | from typing import Callable
78 78 | from typing import cast
UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
|
@@ -959,8 +960,8 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
75 | from typing import Reversible
76 | from typing import Generator
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
77 |
78 | # OK
77 | from typing import Callable
78 | from typing import cast
|
= help: Import from `collections.abc`
@@ -970,23 +971,63 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
75 75 | from typing import Reversible
76 |-from typing import Generator
76 |+from collections.abc import Generator
77 77 |
78 78 | # OK
79 79 | from a import b
77 77 | from typing import Callable
78 78 | from typing import cast
79 79 |
UP035.py:88:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
UP035.py:77:1: UP035 [*] Import from `collections.abc` instead: `Callable`
|
87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 | from typing_extensions import dataclass_transform
75 | from typing import Reversible
76 | from typing import Generator
77 | from typing import Callable
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
78 | from typing import cast
|
= help: Import from `collections.abc`
Suggested fix
74 74 | from typing import AsyncGenerator
75 75 | from typing import Reversible
76 76 | from typing import Generator
77 |-from typing import Callable
77 |+from collections.abc import Callable
78 78 | from typing import cast
79 79 |
80 80 | # OK
UP035.py:87:1: UP035 [*] Import from `typing` instead: `NamedTuple`
|
86 | # Ok: `typing_extensions` contains backported improvements.
87 | from typing_extensions import NamedTuple
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
88 |
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
= help: Import from `typing`
Suggested fix
84 84 | from typing_extensions import SupportsIndex
85 85 |
86 86 | # Ok: `typing_extensions` contains backported improvements.
87 |-from typing_extensions import NamedTuple
87 |+from typing import NamedTuple
88 88 |
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 90 | from typing_extensions import dataclass_transform
UP035.py:90:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 | from typing_extensions import dataclass_transform
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
= help: Import from `typing`
Suggested fix
85 85 | from typing_extensions import NamedTuple
86 86 |
87 87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 |-from typing_extensions import dataclass_transform
88 |+from typing import dataclass_transform
87 87 | from typing_extensions import NamedTuple
88 88 |
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
90 |-from typing_extensions import dataclass_transform
90 |+from typing import dataclass_transform

View File

@@ -69,7 +69,7 @@ UP040.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t
14 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
15 |
16 | # UP040 bounded generic (todo)
16 | # UP040 bounded generic
|
= help: Use the `type` keyword
@@ -80,153 +80,154 @@ UP040.py:14:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t
14 |-x: typing.TypeAlias = list[T]
14 |+type x[T] = list[T]
15 15 |
16 16 | # UP040 bounded generic (todo)
16 16 | # UP040 bounded generic
17 17 | T = typing.TypeVar("T", bound=int)
UP040.py:18:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
16 | # UP040 bounded generic (todo)
16 | # UP040 bounded generic
17 | T = typing.TypeVar("T", bound=int)
18 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
19 |
20 | T = typing.TypeVar("T", int, str)
20 | # UP040 constrained generic
|
= help: Use the `type` keyword
Fix
15 15 |
16 16 | # UP040 bounded generic (todo)
16 16 | # UP040 bounded generic
17 17 | T = typing.TypeVar("T", bound=int)
18 |-x: typing.TypeAlias = list[T]
18 |+type x = list[T]
18 |+type x[T: int] = list[T]
19 19 |
20 20 | T = typing.TypeVar("T", int, str)
21 21 | x: typing.TypeAlias = list[T]
20 20 | # UP040 constrained generic
21 21 | T = typing.TypeVar("T", int, str)
UP040.py:21:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:22:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
20 | T = typing.TypeVar("T", int, str)
21 | x: typing.TypeAlias = list[T]
20 | # UP040 constrained generic
21 | T = typing.TypeVar("T", int, str)
22 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
22 |
23 | # UP040 contravariant generic (todo)
23 |
24 | # UP040 contravariant generic
|
= help: Use the `type` keyword
Fix
18 18 | x: typing.TypeAlias = list[T]
19 19 |
20 20 | T = typing.TypeVar("T", int, str)
21 |-x: typing.TypeAlias = list[T]
21 |+type x = list[T]
22 22 |
23 23 | # UP040 contravariant generic (todo)
24 24 | T = typing.TypeVar("T", contravariant=True)
20 20 | # UP040 constrained generic
21 21 | T = typing.TypeVar("T", int, str)
22 |-x: typing.TypeAlias = list[T]
22 |+type x[T: (int, str)] = list[T]
23 23 |
24 24 | # UP040 contravariant generic
25 25 | T = typing.TypeVar("T", contravariant=True)
UP040.py:25:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:26:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
23 | # UP040 contravariant generic (todo)
24 | T = typing.TypeVar("T", contravariant=True)
25 | x: typing.TypeAlias = list[T]
24 | # UP040 contravariant generic
25 | T = typing.TypeVar("T", contravariant=True)
26 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
26 |
27 | # UP040 covariant generic (todo)
27 |
28 | # UP040 covariant generic
|
= help: Use the `type` keyword
Fix
22 22 |
23 23 | # UP040 contravariant generic (todo)
24 24 | T = typing.TypeVar("T", contravariant=True)
25 |-x: typing.TypeAlias = list[T]
25 |+type x = list[T]
26 26 |
27 27 | # UP040 covariant generic (todo)
28 28 | T = typing.TypeVar("T", covariant=True)
23 23 |
24 24 | # UP040 contravariant generic
25 25 | T = typing.TypeVar("T", contravariant=True)
26 |-x: typing.TypeAlias = list[T]
26 |+type x[T] = list[T]
27 27 |
28 28 | # UP040 covariant generic
29 29 | T = typing.TypeVar("T", covariant=True)
UP040.py:29:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:30:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
27 | # UP040 covariant generic (todo)
28 | T = typing.TypeVar("T", covariant=True)
29 | x: typing.TypeAlias = list[T]
28 | # UP040 covariant generic
29 | T = typing.TypeVar("T", covariant=True)
30 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
30 |
31 | # UP040 in class scope
31 |
32 | # UP040 in class scope
|
= help: Use the `type` keyword
Fix
26 26 |
27 27 | # UP040 covariant generic (todo)
28 28 | T = typing.TypeVar("T", covariant=True)
29 |-x: typing.TypeAlias = list[T]
29 |+type x = list[T]
30 30 |
31 31 | # UP040 in class scope
32 32 | T = typing.TypeVar["T"]
27 27 |
28 28 | # UP040 covariant generic
29 29 | T = typing.TypeVar("T", covariant=True)
30 |-x: typing.TypeAlias = list[T]
30 |+type x[T] = list[T]
31 31 |
32 32 | # UP040 in class scope
33 33 | T = typing.TypeVar["T"]
UP040.py:35:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:36:5: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
33 | class Foo:
34 | # reference to global variable
35 | x: typing.TypeAlias = list[T]
34 | class Foo:
35 | # reference to global variable
36 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
36 |
37 | # reference to class variable
37 |
38 | # reference to class variable
|
= help: Use the `type` keyword
Fix
32 32 | T = typing.TypeVar["T"]
33 33 | class Foo:
34 34 | # reference to global variable
35 |- x: typing.TypeAlias = list[T]
35 |+ type x[T] = list[T]
36 36 |
37 37 | # reference to class variable
38 38 | TCLS = typing.TypeVar["TCLS"]
33 33 | T = typing.TypeVar["T"]
34 34 | class Foo:
35 35 | # reference to global variable
36 |- x: typing.TypeAlias = list[T]
36 |+ type x[T] = list[T]
37 37 |
38 38 | # reference to class variable
39 39 | TCLS = typing.TypeVar["TCLS"]
UP040.py:39:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:40:5: UP040 [*] Type alias `y` uses `TypeAlias` annotation instead of the `type` keyword
|
37 | # reference to class variable
38 | TCLS = typing.TypeVar["TCLS"]
39 | y: typing.TypeAlias = list[TCLS]
38 | # reference to class variable
39 | TCLS = typing.TypeVar["TCLS"]
40 | y: typing.TypeAlias = list[TCLS]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
40 |
41 | # UP040 wont add generics in fix
41 |
42 | # UP040 wont add generics in fix
|
= help: Use the `type` keyword
Fix
36 36 |
37 37 | # reference to class variable
38 38 | TCLS = typing.TypeVar["TCLS"]
39 |- y: typing.TypeAlias = list[TCLS]
39 |+ type y[TCLS] = list[TCLS]
40 40 |
41 41 | # UP040 wont add generics in fix
42 42 | T = typing.TypeVar(*args)
37 37 |
38 38 | # reference to class variable
39 39 | TCLS = typing.TypeVar["TCLS"]
40 |- y: typing.TypeAlias = list[TCLS]
40 |+ type y[TCLS] = list[TCLS]
41 41 |
42 42 | # UP040 wont add generics in fix
43 43 | T = typing.TypeVar(*args)
UP040.py:43:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
UP040.py:44:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
|
41 | # UP040 wont add generics in fix
42 | T = typing.TypeVar(*args)
43 | x: typing.TypeAlias = list[T]
42 | # UP040 wont add generics in fix
43 | T = typing.TypeVar(*args)
44 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
44 |
45 | # OK
45 |
46 | # OK
|
= help: Use the `type` keyword
Fix
40 40 |
41 41 | # UP040 wont add generics in fix
42 42 | T = typing.TypeVar(*args)
43 |-x: typing.TypeAlias = list[T]
43 |+type x = list[T]
44 44 |
45 45 | # OK
46 46 | x: TypeAlias
41 41 |
42 42 | # UP040 wont add generics in fix
43 43 | T = typing.TypeVar(*args)
44 |-x: typing.TypeAlias = list[T]
44 |+type x = list[T]
45 45 |
46 46 | # OK
47 47 | x: TypeAlias

View File

@@ -0,0 +1,36 @@
use ruff_python_ast as ast;
use ruff_python_codegen::Generator;
use ruff_text_size::TextRange;
/// Format a code snippet to call `name.method()`.
pub(super) fn generate_method_call(name: &str, method: &str, generator: Generator) -> String {
// Construct `name`.
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Construct `name.method`.
let attr = ast::ExprAttribute {
value: Box::new(var.into()),
attr: ast::Identifier::new(method.to_string(), TextRange::default()),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make it into a call `name.method()`
let call = ast::ExprCall {
func: Box::new(attr.into()),
arguments: ast::Arguments {
args: vec![],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
};
// And finally, turn it into a statement.
let stmt = ast::StmtExpr {
value: Box::new(call.into()),
range: TextRange::default(),
};
generator.stmt(&stmt.into())
}

View File

@@ -1,5 +1,6 @@
//! Rules from [refurb](https://pypi.org/project/refurb/)/
mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -16,6 +17,7 @@ mod tests {
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -12,12 +12,17 @@ use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `set#remove` that can be replaced with `set#discard`.
/// Checks for uses of `set.remove` that can be replaced with `set.discard`.
///
/// ## Why is this bad?
/// If an element should be removed from a set if it is present, it is more
/// succinct and idiomatic to use `discard`.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect sets that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// nums = {123, 456}

View File

@@ -1,13 +1,13 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Generator;
use ruff_python_semantic::analyze::typing::{is_dict, is_list};
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::refurb::helpers::generate_method_call;
/// ## What it does
/// Checks for `del` statements that delete the entire slice of a list or
@@ -17,6 +17,11 @@ use crate::registry::AsRule;
/// It's is faster and more succinct to remove all items via the `clear()`
/// method.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists and dictionaries that are instantiated as
/// literals or annotated with a type annotation.
///
/// ## Example
/// ```python
/// names = {"key": "value"}
@@ -65,7 +70,7 @@ pub(crate) fn delete_full_slice(checker: &mut Checker, delete: &ast::StmtDelete)
// Fix is only supported for single-target deletions.
if checker.patch(diagnostic.kind.rule()) && delete.targets.len() == 1 {
let replacement = make_suggestion(name, checker.generator());
let replacement = generate_method_call(name, "clear", checker.generator());
diagnostic.set_fix(Fix::suggested(Edit::replacement(
replacement,
delete.start(),
@@ -118,38 +123,3 @@ fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a
// Name is needed for the fix suggestion.
Some(name)
}
/// Make fix suggestion for the given name, ie `name.clear()`.
fn make_suggestion(name: &str, generator: Generator) -> String {
// Here we construct `var.clear()`
//
// And start with construction of `var`
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make `var.clear`.
let attr = ast::ExprAttribute {
value: Box::new(var.into()),
attr: ast::Identifier::new("clear".to_string(), TextRange::default()),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Make it into a call `var.clear()`
let call = ast::ExprCall {
func: Box::new(attr.into()),
arguments: ast::Arguments {
args: vec![],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
};
// And finally, turn it into a statement.
let stmt = ast::StmtExpr {
value: Box::new(call.into()),
range: TextRange::default(),
};
generator.stmt(&stmt.into())
}

View File

@@ -1,7 +1,9 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use repeated_append::*;
pub(crate) use slice_copy::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod repeated_append;
mod slice_copy;

View File

@@ -21,6 +21,11 @@ use crate::registry::AsRule;
/// a single `extend`. Each `append` resizes the list individually, whereas an
/// `extend` can resize the list once for all elements.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// nums = [1, 2, 3]

View File

@@ -0,0 +1,109 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::is_list;
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::refurb::helpers::generate_method_call;
/// ## What it does
/// Checks for unbounded slice expressions to copy a list.
///
/// ## Why is this bad?
/// The `list.copy` method is more readable and consistent with copying other
/// types.
///
/// ## Known problems
/// This rule is prone to false negatives due to type inference limitations,
/// as it will only detect lists that are instantiated as literals or annotated
/// with a type annotation.
///
/// ## Example
/// ```python
/// a = [1, 2, 3]
/// b = a[:]
/// ```
///
/// Use instead:
/// ```python
/// a = [1, 2, 3]
/// b = a.copy()
/// ```
///
/// ## References
/// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types)
#[violation]
pub struct SliceCopy;
impl Violation for SliceCopy {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Prefer `copy` method over slicing")
}
fn autofix_title(&self) -> Option<String> {
Some("Replace with `copy()`".to_string())
}
}
/// FURB145
pub(crate) fn slice_copy(checker: &mut Checker, subscript: &ast::ExprSubscript) {
if subscript.ctx.is_store() || subscript.ctx.is_del() {
return;
}
let Some(name) = match_list_full_slice(subscript, checker.semantic()) else {
return;
};
let mut diagnostic = Diagnostic::new(SliceCopy, subscript.range());
if checker.patch(diagnostic.kind.rule()) {
let replacement = generate_method_call(name, "copy", checker.generator());
diagnostic.set_fix(Fix::suggested(Edit::replacement(
replacement,
subscript.start(),
subscript.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
/// Matches `obj[:]` where `obj` is a list.
fn match_list_full_slice<'a>(
subscript: &'a ast::ExprSubscript,
semantic: &SemanticModel,
) -> Option<&'a str> {
// Check that it is `obj[:]`.
if !matches!(
subscript.slice.as_ref(),
Expr::Slice(ast::ExprSlice {
lower: None,
upper: None,
step: None,
range: _,
})
) {
return None;
}
let ast::ExprName { id, .. } = subscript.value.as_name_expr()?;
// Check that `obj` is a list.
let scope = semantic.current_scope();
let bindings: Vec<&Binding> = scope
.get_all(id)
.map(|binding_id| semantic.binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
return None;
};
if !is_list(binding, semantic) {
return None;
}
Some(id)
}

View File

@@ -0,0 +1,128 @@
---
source: crates/ruff/src/rules/refurb/mod.rs
---
FURB145.py:4:5: FURB145 [*] Prefer `copy` method over slicing
|
3 | # Errors.
4 | a = l[:]
| ^^^^ FURB145
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
|
= help: Replace with `copy()`
Suggested fix
1 1 | l = [1, 2, 3, 4, 5]
2 2 |
3 3 | # Errors.
4 |-a = l[:]
4 |+a = l.copy()
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 7 | m = l[::]
FURB145.py:5:11: FURB145 [*] Prefer `copy` method over slicing
|
3 | # Errors.
4 | a = l[:]
5 | b, c = 1, l[:]
| ^^^^ FURB145
6 | d, e = l[:], 1
7 | m = l[::]
|
= help: Replace with `copy()`
Suggested fix
2 2 |
3 3 | # Errors.
4 4 | a = l[:]
5 |-b, c = 1, l[:]
5 |+b, c = 1, l.copy()
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 8 | l[:]
FURB145.py:6:8: FURB145 [*] Prefer `copy` method over slicing
|
4 | a = l[:]
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
| ^^^^ FURB145
7 | m = l[::]
8 | l[:]
|
= help: Replace with `copy()`
Suggested fix
3 3 | # Errors.
4 4 | a = l[:]
5 5 | b, c = 1, l[:]
6 |-d, e = l[:], 1
6 |+d, e = l.copy(), 1
7 7 | m = l[::]
8 8 | l[:]
9 9 | print(l[:])
FURB145.py:7:5: FURB145 [*] Prefer `copy` method over slicing
|
5 | b, c = 1, l[:]
6 | d, e = l[:], 1
7 | m = l[::]
| ^^^^^ FURB145
8 | l[:]
9 | print(l[:])
|
= help: Replace with `copy()`
Suggested fix
4 4 | a = l[:]
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 |-m = l[::]
7 |+m = l.copy()
8 8 | l[:]
9 9 | print(l[:])
10 10 |
FURB145.py:8:1: FURB145 [*] Prefer `copy` method over slicing
|
6 | d, e = l[:], 1
7 | m = l[::]
8 | l[:]
| ^^^^ FURB145
9 | print(l[:])
|
= help: Replace with `copy()`
Suggested fix
5 5 | b, c = 1, l[:]
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 |-l[:]
8 |+l.copy()
9 9 | print(l[:])
10 10 |
11 11 | # False negatives.
FURB145.py:9:7: FURB145 [*] Prefer `copy` method over slicing
|
7 | m = l[::]
8 | l[:]
9 | print(l[:])
| ^^^^ FURB145
10 |
11 | # False negatives.
|
= help: Replace with `copy()`
Suggested fix
6 6 | d, e = l[:], 1
7 7 | m = l[::]
8 8 | l[:]
9 |-print(l[:])
9 |+print(l.copy())
10 10 |
11 11 | # False negatives.
12 12 | aa = a[:] # Type inference.

View File

@@ -50,7 +50,7 @@ ruff_python_parser = { path = "../ruff_python_parser" }
codspeed = ["codspeed-criterion-compat"]
[target.'cfg(target_os = "windows")'.dev-dependencies]
mimalloc = "0.1.34"
mimalloc = "0.1.39"
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies]
tikv-jemallocator = "0.5.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.289"
version = "0.0.290"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -36,7 +36,7 @@ ruff_text_size = { path = "../ruff_text_size" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
argfile = { version = "0.1.5" }
argfile = { version = "0.1.6" }
bincode = { version = "1.3.3" }
bitflags = { workspace = true }
cachedir = { version = "0.3.0" }
@@ -52,7 +52,7 @@ is-macro = { workspace = true }
itertools = { workspace = true }
itoa = { version = "1.0.6" }
log = { workspace = true }
notify = { version = "5.1.0" }
notify = { version = "6.1.1" }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { version = "1.7.0" }
regex = { workspace = true }
@@ -79,7 +79,7 @@ test-case = { workspace = true }
ureq = { version = "2.6.2", features = [] }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = "0.1.34"
mimalloc = "0.1.39"
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = "0.5.0"

View File

@@ -1,17 +1,16 @@
use std::path::PathBuf;
use std::str::FromStr;
use clap::{command, Parser};
use regex::Regex;
use ruff::line_width::LineLength;
use rustc_hash::FxHashMap;
use ruff::line_width::LineLength;
use ruff::logging::LogLevel;
use ruff::registry::Rule;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
};
use ruff::RuleSelector;
use ruff::{RuleSelector, RuleSelectorParser};
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::resolver::ConfigProcessor;
@@ -129,7 +128,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -139,7 +138,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -149,7 +148,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -159,7 +158,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide = true
)]
@@ -191,7 +190,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -201,7 +200,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -211,7 +210,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
@@ -221,7 +220,7 @@ pub struct CheckCommand {
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide = true
)]
@@ -511,11 +510,6 @@ impl FormatCommand {
}
}
fn parse_rule_selector(env: &str) -> Result<RuleSelector, std::io::Error> {
RuleSelector::from_str(env)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
}
fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
match (yes, no) {
(true, false) => Some(true),

View File

@@ -211,16 +211,16 @@ fn lint_path(
match result {
Ok(inner) => inner,
Err(error) => {
let message = r#"This indicates a bug in `ruff`. If you could open an issue at:
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#;
warn!(
error!(
"{}{}{} {message}\n{error}",
"Linting panicked ".bold(),
"Panicked while linting ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);

View File

@@ -6,6 +6,7 @@ use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use log::error;
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
@@ -22,6 +23,7 @@ use ruff_source_file::{find_newline, LineEnding};
use ruff_workspace::resolver::python_files_in_path;
use crate::args::{FormatArguments, Overrides};
use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve;
use crate::ExitStatus;
@@ -85,7 +87,13 @@ pub(crate) fn format(
.with_line_width(LineWidth::from(NonZeroU16::from(line_length)))
.with_preview(preview);
debug!("Formatting {} with {:?}", path.display(), options);
Some(format_path(path, options, mode))
Some(match catch_unwind(|| format_path(path, options, mode)) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
})
}
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
}
@@ -95,22 +103,20 @@ pub(crate) fn format(
Err(err) => Right(err),
});
let duration = start.elapsed();
debug!(
"Formatted {} files in {:.2?}",
results.len() + errors.len(),
duration
);
let summary = FormatResultSummary::new(results, mode);
// Report on any errors.
if !errors.is_empty() {
warn!("Encountered {} errors while formatting:", errors.len());
for error in &errors {
warn!("{error}");
}
for error in &errors {
error!("{error}");
}
let summary = FormatResultSummary::new(results, mode);
// Report on the formatting changes.
if log_level >= LogLevel::Default {
#[allow(clippy::print_stdout)]
@@ -255,6 +261,7 @@ pub(crate) enum FormatCommandError {
Read(Option<PathBuf>, io::Error),
Write(Option<PathBuf>, io::Error),
FormatModule(Option<PathBuf>, FormatModuleError),
Panic(Option<PathBuf>, PanicError),
}
impl Display for FormatCommandError {
@@ -320,6 +327,29 @@ impl Display for FormatCommandError {
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
}
}
Self::Panic(path, err) => {
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BFormatter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#;
if let Some(path) = path {
write!(
f,
"{}{}{} {message}\n{err}",
"Panicked while formatting ".bold(),
fs::relativize_path(path).bold(),
":".bold()
)
} else {
write!(
f,
"{} {message}\n{err}",
"Panicked while formatting.".bold()
)
}
}
}
}
}

View File

@@ -111,13 +111,15 @@ pub fn run(
{
eprintln!(
r#"
{}: `ruff` crashed. This indicates a bug in `ruff`. If you could open an issue at:
{}{} {} If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BPanic%5D
https://github.com/astral-sh/ruff/issues/new?title=%5BPanic%5D
quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
...quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
"#,
"error".red().bold(),
":".bold(),
"Ruff crashed.".bold(),
);
}
default_panic_hook(info);

View File

@@ -301,6 +301,178 @@ fn nursery_direct() {
Found 1 error.
----- stderr -----
warning: Selection of nursery rule `E225` without the `--preview` flag is deprecated.
"###);
}
#[test]
fn nursery_group_selector() {
// Only nursery rules should be detected e.g. E225 and a warning should be displayed
let args = ["--select", "NURSERY"];
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: CPY001 Missing copyright notice at top of file
-:1:2: E225 Missing whitespace around operator
Found 2 errors.
----- stderr -----
warning: The `NURSERY` selector has been deprecated. Use the `--preview` flag instead.
"###);
}
#[test]
fn nursery_group_selector_preview_enabled() {
// Only nursery rules should be detected e.g. E225 and a warning should be displayed
let args = ["--select", "NURSERY", "--preview"];
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: CPY001 Missing copyright notice at top of file
-:1:2: E225 Missing whitespace around operator
Found 2 errors.
----- stderr -----
warning: The `NURSERY` selector has been deprecated.
"###);
}
#[test]
fn preview_enabled_prefix() {
// E741 and E225 (preview) should both be detected
let args = ["--select", "E", "--preview"];
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`
-:1:2: E225 Missing whitespace around operator
Found 2 errors.
----- stderr -----
"###);
}
#[test]
fn preview_enabled_all() {
let args = ["--select", "ALL", "--preview"];
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`
-:1:1: D100 Missing docstring in public module
-:1:1: CPY001 Missing copyright notice at top of file
-:1:2: E225 Missing whitespace around operator
Found 4 errors.
----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
"###);
}
#[test]
fn preview_enabled_direct() {
// E225 should be detected without warning
let args = ["--select", "E225", "--preview"];
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:2: E225 Missing whitespace around operator
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn preview_disabled_direct() {
// FURB145 is preview not nursery so selecting should be empty
let args = ["--select", "FURB145"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("a = l[:]\n"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Selection `FURB145` has no effect because the `--preview` flag was not included.
"###);
}
#[test]
fn preview_disabled_prefix_empty() {
// Warns that the selection is empty since all of the CPY rules are in preview
let args = ["--select", "CPY"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin("I=42\n"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Selection `CPY` has no effect because the `--preview` flag was not included.
"###);
}
#[test]
fn preview_group_selector() {
// `--select PREVIEW` should error (selector was removed)
let args = ["--select", "PREVIEW", "--preview"];
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: 2
----- stdout -----
----- stderr -----
error: invalid value 'PREVIEW' for '--select <RULE_CODE>'
For more information, try '--help'.
"###);
}
#[test]
fn preview_enabled_group_ignore() {
// `--select E --ignore PREVIEW` should detect E741 and E225, which is in preview but "E" is more specific.
let args = ["--select", "E", "--ignore", "PREVIEW", "--preview"];
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: 2
----- stdout -----
----- stderr -----
error: invalid value 'PREVIEW' for '--ignore <RULE_CODE>'
For more information, try '--help'.
"###);
}

View File

@@ -25,7 +25,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
}
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
};
let preview_token = if rule.is_preview() {
let preview_token = if rule.is_preview() || rule.is_nursery() {
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
} else {
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")

View File

@@ -361,7 +361,7 @@ where
f.write_element(FormatElement::Text {
text: self.text.to_string().into_boxed_str(),
text_width: TextWidth::from_text(self.text, f.options().tab_width()),
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
});
Ok(())
@@ -393,8 +393,10 @@ where
let slice = source_code.slice(self.range);
debug_assert_no_newlines(slice.text(source_code));
let text_width =
TextWidth::from_text(slice.text(source_code), f.context().options().tab_width());
let text_width = TextWidth::from_text(
slice.text(source_code),
f.context().options().indent_width(),
);
f.write_element(FormatElement::SourceCodeSlice { slice, text_width });
@@ -917,8 +919,10 @@ where
/// use ruff_formatter::prelude::*;
///
/// # fn main() -> FormatResult<()> {
/// use ruff_formatter::IndentWidth;
/// let context = SimpleFormatContext::new(SimpleFormatOptions {
/// indent_style: IndentStyle::Space(4),
/// indent_style: IndentStyle::Space,
/// indent_width: IndentWidth::try_from(4).unwrap(),
/// ..SimpleFormatOptions::default()
/// });
///

View File

@@ -10,7 +10,7 @@ use unicode_width::UnicodeWidthChar;
use crate::format_element::tag::{GroupMode, LabelId, Tag};
use crate::source_code::SourceCodeSlice;
use crate::{TabWidth, TagKind};
use crate::{IndentWidth, TagKind};
use ruff_text_size::TextSize;
/// Language agnostic IR for formatting source code.
@@ -432,12 +432,12 @@ pub enum TextWidth {
}
impl TextWidth {
pub fn from_text(text: &str, tab_width: TabWidth) -> TextWidth {
pub fn from_text(text: &str, indent_width: IndentWidth) -> TextWidth {
let mut width = 0u32;
for c in text.chars() {
let char_width = match c {
'\t' => tab_width.value(),
'\t' => indent_width.value(),
'\n' => return TextWidth::Multiline,
#[allow(clippy::cast_possible_truncation)]
c => c.width().unwrap_or(0) as u32,

View File

@@ -9,7 +9,7 @@ use crate::prelude::*;
use crate::source_code::SourceCode;
use crate::{
format, write, BufferExtensions, Format, FormatContext, FormatElement, FormatOptions,
FormatResult, Formatter, IndentStyle, LineWidth, PrinterOptions, TabWidth,
FormatResult, Formatter, IndentStyle, IndentWidth, LineWidth, PrinterOptions,
};
use super::tag::Tag;
@@ -213,11 +213,11 @@ struct IrFormatOptions;
impl FormatOptions for IrFormatOptions {
fn indent_style(&self) -> IndentStyle {
IndentStyle::Space(2)
IndentStyle::Space
}
fn tab_width(&self) -> TabWidth {
TabWidth::default()
fn indent_width(&self) -> IndentWidth {
IndentWidth::default()
}
fn line_width(&self) -> LineWidth {
@@ -227,7 +227,7 @@ impl FormatOptions for IrFormatOptions {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
line_width: self.line_width(),
indent_style: IndentStyle::Space(2),
indent_style: IndentStyle::Space,
..PrinterOptions::default()
}
}

View File

@@ -52,23 +52,20 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
pub use group_id::GroupId;
use ruff_text_size::{TextRange, TextSize};
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum IndentStyle {
/// Tab
/// Use tabs to indent code.
#[default]
Tab,
/// Space, with its quantity
Space(u8),
/// Use [`IndentWidth`] spaces to indent code.
Space,
}
impl IndentStyle {
pub const DEFAULT_SPACES: u8 = 2;
/// Returns `true` if this is an [`IndentStyle::Tab`].
pub const fn is_tab(&self) -> bool {
matches!(self, IndentStyle::Tab)
@@ -76,58 +73,42 @@ impl IndentStyle {
/// Returns `true` if this is an [`IndentStyle::Space`].
pub const fn is_space(&self) -> bool {
matches!(self, IndentStyle::Space(_))
}
}
impl FromStr for IndentStyle {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"tab" | "Tabs" => Ok(Self::Tab),
"space" | "Spaces" => Ok(Self::Space(IndentStyle::DEFAULT_SPACES)),
// TODO: replace this error with a diagnostic
v => {
let v = v.strip_prefix("Spaces, size: ").unwrap_or(v);
u8::from_str(v)
.map(Self::Space)
.map_err(|_| "Value not supported for IndentStyle")
}
}
matches!(self, IndentStyle::Space)
}
}
impl std::fmt::Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndentStyle::Tab => std::write!(f, "Tab"),
IndentStyle::Space(size) => std::write!(f, "Spaces, size: {size}"),
IndentStyle::Tab => std::write!(f, "tab"),
IndentStyle::Space => std::write!(f, "space"),
}
}
}
/// The visual width of a `\t` character.
/// The visual width of a indentation.
///
/// Determines the visual width of a tab character (`\t`) and the number of
/// spaces per indent when using [`IndentStyle::Space`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabWidth(NonZeroU8);
pub struct IndentWidth(NonZeroU8);
impl TabWidth {
impl IndentWidth {
/// Return the numeric value for this [`LineWidth`]
pub const fn value(&self) -> u32 {
self.0.get() as u32
}
}
impl Default for TabWidth {
impl Default for IndentWidth {
fn default() -> Self {
Self(NonZeroU8::new(2).unwrap())
}
}
impl TryFrom<u8> for TabWidth {
impl TryFrom<u8> for IndentWidth {
type Error = TryFromIntError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
@@ -196,16 +177,8 @@ pub trait FormatOptions {
/// The indent style.
fn indent_style(&self) -> IndentStyle;
/// The visual width of a tab character.
fn tab_width(&self) -> TabWidth;
/// The visual width of an indent
fn indent_width(&self) -> u32 {
match self.indent_style() {
IndentStyle::Tab => self.tab_width().value(),
IndentStyle::Space(spaces) => u32::from(spaces),
}
}
fn indent_width(&self) -> IndentWidth;
/// What's the max width of a line. Defaults to 80.
fn line_width(&self) -> LineWidth;
@@ -250,6 +223,7 @@ impl FormatContext for SimpleFormatContext {
#[derive(Debug, Default, Eq, PartialEq, Clone)]
pub struct SimpleFormatOptions {
pub indent_style: IndentStyle,
pub indent_width: IndentWidth,
pub line_width: LineWidth,
}
@@ -258,8 +232,8 @@ impl FormatOptions for SimpleFormatOptions {
self.indent_style
}
fn tab_width(&self) -> TabWidth {
TabWidth::default()
fn indent_width(&self) -> IndentWidth {
self.indent_width
}
fn line_width(&self) -> LineWidth {
@@ -270,6 +244,7 @@ impl FormatOptions for SimpleFormatOptions {
PrinterOptions {
line_width: self.line_width,
indent_style: self.indent_style,
indent_width: self.indent_width,
source_map_generation: SourceMapGeneration::Enabled,
..PrinterOptions::default()
}

View File

@@ -367,7 +367,7 @@ impl<'a> Printer<'a> {
if !self.state.pending_indent.is_empty() {
let (indent_char, repeat_count) = match self.options.indent_style() {
IndentStyle::Tab => ('\t', 1),
IndentStyle::Space(count) => (' ', count),
IndentStyle::Space => (' ', self.options.indent_width()),
};
let indent = std::mem::take(&mut self.state.pending_indent);
@@ -764,7 +764,7 @@ impl<'a> Printer<'a> {
#[allow(clippy::cast_possible_truncation)]
let char_width = if char == '\t' {
self.options.tab_width.value()
self.options.indent_width.value()
} else {
// SAFETY: A u32 is sufficient to represent the width of a file <= 4GB
char.width().unwrap_or(0) as u32
@@ -1347,7 +1347,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
} else {
for c in text.chars() {
let char_width = match c {
'\t' => self.options().tab_width.value(),
'\t' => self.options().indent_width.value(),
'\n' => {
if self.must_be_flat {
return Fits::No;
@@ -1501,7 +1501,7 @@ mod tests {
use crate::printer::{LineEnding, Printer, PrinterOptions};
use crate::source_code::SourceCode;
use crate::{
format_args, write, Document, FormatState, IndentStyle, LineWidth, Printed, TabWidth,
format_args, write, Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed,
VecBuffer,
};
@@ -1509,7 +1509,7 @@ mod tests {
format_with_options(
root,
PrinterOptions {
indent_style: IndentStyle::Space(2),
indent_style: IndentStyle::Space,
..PrinterOptions::default()
},
)
@@ -1653,7 +1653,7 @@ two lines`,
fn it_use_the_indent_character_specified_in_the_options() {
let options = PrinterOptions {
indent_style: IndentStyle::Tab,
tab_width: TabWidth::try_from(4).unwrap(),
indent_width: IndentWidth::try_from(4).unwrap(),
line_width: LineWidth::try_from(19).unwrap(),
..PrinterOptions::default()
};

View File

@@ -1,10 +1,13 @@
use crate::{FormatOptions, IndentStyle, LineWidth, TabWidth};
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct PrinterOptions {
/// Width of a single tab character (does it equal 2, 4, ... spaces?)
pub tab_width: TabWidth,
pub indent_width: IndentWidth,
/// Whether the printer should use tabs or spaces to indent code.
pub indent_style: IndentStyle,
/// What's the max width of a line. Defaults to 80
pub line_width: LineWidth,
@@ -12,9 +15,6 @@ pub struct PrinterOptions {
/// The type of line ending to apply to the printed input
pub line_ending: LineEnding,
/// Whether the printer should use tabs or spaces to indent code and if spaces, by how many.
pub indent_style: IndentStyle,
/// Whether the printer should build a source map that allows mapping positions in the source document
/// to positions in the formatted document.
pub source_map_generation: SourceMapGeneration,
@@ -46,8 +46,8 @@ impl PrinterOptions {
}
#[must_use]
pub fn with_tab_width(mut self, width: TabWidth) -> Self {
self.tab_width = width;
pub fn with_tab_width(mut self, width: IndentWidth) -> Self {
self.indent_width = width;
self
}
@@ -58,10 +58,7 @@ impl PrinterOptions {
/// Width of an indent in characters.
pub(super) const fn indent_width(&self) -> u32 {
match self.indent_style {
IndentStyle::Tab => self.tab_width.value(),
IndentStyle::Space(count) => count as u32,
}
self.indent_width.value()
}
}

View File

@@ -22,7 +22,7 @@ itertools = { workspace = true }
once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { version = "3.0.0" }
serde_with = { version = "3.0.0", default-features = false, features = ["macros"] }
thiserror = { workspace = true }
uuid = { workspace = true }

View File

@@ -19,6 +19,7 @@ ruff_text_size = { path = "../ruff_text_size" }
bitflags = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }

View File

@@ -1,25 +1,23 @@
//! An equivalent object hierarchy to the `RustPython` AST hierarchy, but with the
//! ability to compare expressions for equality (via [`Eq`] and [`Hash`]).
//!
//! Two [`ComparableExpr`]s are considered equal if the underlying AST nodes have the
//! same shape, ignoring trivia (e.g., parentheses, comments, and whitespace), the
//! location in the source code, and other contextual information (e.g., whether they
//! represent reads or writes, which is typically encoded in the Python AST).
//!
//! For example, in `[(a, b) for a, b in c]`, the `(a, b)` and `a, b` expressions are
//! considered equal, despite the former being parenthesized, and despite the former
//! being a write ([`ast::ExprContext::Store`]) and the latter being a read
//! ([`ast::ExprContext::Load`]).
//!
//! Similarly, `"a" "b"` and `"ab"` would be considered equal, despite the former being
//! an implicit concatenation of string literals, as these expressions are considered to
//! have the same shape in that they evaluate to the same value.
use crate as ast;
use num_bigint::BigInt;
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum ComparableExprContext {
Load,
Store,
Del,
}
impl From<&ast::ExprContext> for ComparableExprContext {
fn from(ctx: &ast::ExprContext) -> Self {
match ctx {
ast::ExprContext::Load => Self::Load,
ast::ExprContext::Store => Self::Store,
ast::ExprContext::Del => Self::Del,
}
}
}
use crate as ast;
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum ComparableBoolOp {
@@ -665,38 +663,32 @@ pub struct ExprConstant<'a> {
pub struct ExprAttribute<'a> {
value: Box<ComparableExpr<'a>>,
attr: &'a str,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprSubscript<'a> {
value: Box<ComparableExpr<'a>>,
slice: Box<ComparableExpr<'a>>,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprStarred<'a> {
value: Box<ComparableExpr<'a>>,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprName<'a> {
id: &'a str,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprList<'a> {
elts: Vec<ComparableExpr<'a>>,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprTuple<'a> {
elts: Vec<ComparableExpr<'a>>,
ctx: ComparableExprContext,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -915,50 +907,46 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
ast::Expr::Attribute(ast::ExprAttribute {
value,
attr,
ctx,
ctx: _,
range: _,
}) => Self::Attribute(ExprAttribute {
value: value.into(),
attr: attr.as_str(),
ctx: ctx.into(),
}),
ast::Expr::Subscript(ast::ExprSubscript {
value,
slice,
ctx,
ctx: _,
range: _,
}) => Self::Subscript(ExprSubscript {
value: value.into(),
slice: slice.into(),
ctx: ctx.into(),
}),
ast::Expr::Starred(ast::ExprStarred {
value,
ctx,
ctx: _,
range: _,
}) => Self::Starred(ExprStarred {
value: value.into(),
ctx: ctx.into(),
}),
ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => Self::Name(ExprName {
id: id.as_str(),
ctx: ctx.into(),
}),
ast::Expr::Name(ast::ExprName {
id,
ctx: _,
range: _,
}) => Self::Name(ExprName { id: id.as_str() }),
ast::Expr::List(ast::ExprList {
elts,
ctx,
ctx: _,
range: _,
}) => Self::List(ExprList {
elts: elts.iter().map(Into::into).collect(),
ctx: ctx.into(),
}),
ast::Expr::Tuple(ast::ExprTuple {
elts,
ctx,
ctx: _,
range: _,
}) => Self::Tuple(ExprTuple {
elts: elts.iter().map(Into::into).collect(),
ctx: ctx.into(),
}),
ast::Expr::Slice(ast::ExprSlice {
lower,

View File

@@ -207,6 +207,8 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
range: _,
}) => {
any_over_expr(call_func, func)
// Note that this is the evaluation order but not necessarily the declaration order
// (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not)
|| args.iter().any(|expr| any_over_expr(expr, func))
|| keywords
.iter()
@@ -347,6 +349,8 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool {
decorator_list,
..
}) => {
// Note that e.g. `class A(*args, a=2, *args2, **kwargs): pass` is a valid class
// definition
arguments
.as_deref()
.is_some_and(|Arguments { args, keywords, .. }| {

View File

@@ -1,9 +1,9 @@
use crate::visitor::preorder::PreorderVisitor;
use crate::{
self as ast, Alias, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword,
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments,
PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
TypeParams, WithItem,
self as ast, Alias, ArgOrKeyword, Arguments, Comprehension, Decorator, ExceptHandler, Expr,
Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern,
PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, WithItem,
};
use ruff_text_size::{Ranged, TextRange};
use std::ptr::NonNull;
@@ -3549,18 +3549,11 @@ impl AstNode for Arguments {
where
V: PreorderVisitor<'a> + ?Sized,
{
let ast::Arguments {
range: _,
args,
keywords,
} = self;
for arg in args {
visitor.visit_expr(arg);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
for arg_or_keyword in self.arguments_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => visitor.visit_expr(arg),
ArgOrKeyword::Keyword(keyword) => visitor.visit_keyword(keyword),
}
}
}
}

View File

@@ -1,5 +1,6 @@
#![allow(clippy::derive_partial_eq_without_eq)]
use itertools::Itertools;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
@@ -2177,6 +2178,34 @@ pub struct Arguments {
pub keywords: Vec<Keyword>,
}
/// An entry in the argument list of a function call.
#[derive(Clone, Debug, PartialEq)]
pub enum ArgOrKeyword<'a> {
Arg(&'a Expr),
Keyword(&'a Keyword),
}
impl<'a> From<&'a Expr> for ArgOrKeyword<'a> {
fn from(arg: &'a Expr) -> Self {
Self::Arg(arg)
}
}
impl<'a> From<&'a Keyword> for ArgOrKeyword<'a> {
fn from(keyword: &'a Keyword) -> Self {
Self::Keyword(keyword)
}
}
impl Ranged for ArgOrKeyword<'_> {
fn range(&self) -> TextRange {
match self {
Self::Arg(arg) => arg.range(),
Self::Keyword(keyword) => keyword.range(),
}
}
}
impl Arguments {
/// Return the number of positional and keyword arguments.
pub fn len(&self) -> usize {
@@ -2212,6 +2241,46 @@ impl Arguments {
.map(|keyword| &keyword.value)
.or_else(|| self.find_positional(position))
}
/// Return the positional and keyword arguments in the order of declaration.
///
/// Positional arguments are generally before keyword arguments, but star arguments are an
/// exception:
/// ```python
/// class A(*args, a=2, *args2, **kwargs):
/// pass
///
/// f(*args, a=2, *args2, **kwargs)
/// ```
/// where `*args` and `args2` are `args` while `a=1` and `kwargs` are `keywords`.
///
/// If you would just chain `args` and `keywords` the call would get reordered which we don't
/// want. This function instead "merge sorts" them into the correct order.
///
/// Note that the order of evaluation is always first `args`, then `keywords`:
/// ```python
/// def f(*args, **kwargs):
/// pass
///
/// def g(x):
/// print(x)
/// return x
///
///
/// f(*g([1]), a=g(2), *g([3]), **g({"4": 5}))
/// ```
/// Output:
/// ```text
/// [1]
/// [3]
/// 2
/// {'4': 5}
/// ```
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
let args = self.args.iter().map(ArgOrKeyword::Arg);
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
args.merge_by(keywords, |left, right| left.start() < right.start())
}
}
/// An AST node used to represent a sequence of type parameters.

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