Compare commits

...

47 Commits

Author SHA1 Message Date
Charlie Marsh
244b923f61 Add necessary permissions for cargo-dist Docker build (#12072) 2024-06-27 17:16:05 +02:00
Micha Reiser
a8b48fce7e Release v0.5.0 (#12068)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-06-27 14:46:44 +00:00
Charlie Marsh
04c8597b8a [flake8-simplify] Stabilize detection of Yoda conditions for "constant" collections (SIM300) (#12050)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-06-27 13:44:11 +02:00
Alex Waygood
4029a25ebd [Ruff v0.5] Stabilise 15 pylint rules (#12051) 2024-06-27 13:44:11 +02:00
Micha Reiser
0917ce16f4 Update documentation to mention etcetera crate instead of dirs for user configuration discovery (#12064) 2024-06-27 13:44:11 +02:00
Dhruv Manilawala
22cebdf29b Add server config to filter out syntax error diagnostics (#12059)
## Summary

Follow-up from #11901 

This PR adds a new server setting to show / hide syntax errors.

## Test Plan

### VS Code

Using https://github.com/astral-sh/ruff-vscode/pull/504 with the
following config:

```json
{
  "ruff.nativeServer": true,
  "ruff.path": ["/Users/dhruv/work/astral/ruff/target/debug/ruff"],
  "ruff.showSyntaxErrors": true
}
```

First, set `ruff.showSyntaxErrors` to `true`:
<img width="1177" alt="Screenshot 2024-06-27 at 08 34 58"
src="https://github.com/astral-sh/ruff/assets/67177269/5d77547a-a908-4a00-8714-7c00784e8679">

And then set it to `false`:
<img width="1185" alt="Screenshot 2024-06-27 at 08 35 19"
src="https://github.com/astral-sh/ruff/assets/67177269/9720f089-f10c-420b-a2c1-2bbb2245be35">

### Neovim

Using the following Ruff server config:

```lua
require('lspconfig').ruff.setup {
  init_options = {
    settings = {
      showSyntaxErrors = false,
    },
  },
}
```

First, set `showSyntaxErrors` to `true`:
<img width="1279" alt="Screenshot 2024-06-27 at 08 28 03"
src="https://github.com/astral-sh/ruff/assets/67177269/e694e231-91ba-47f8-8e8a-ad2e82b85a45">

And then set it to `false`:
<img width="1284" alt="Screenshot 2024-06-27 at 08 28 20"
src="https://github.com/astral-sh/ruff/assets/67177269/25b86a57-02b1-44f7-9f65-cf5fdde93b0c">
2024-06-27 13:44:11 +02:00
Dhruv Manilawala
72b6c26101 Simplify LinterResult, avoid cloning ParseError (#11903)
## Summary

Follow-up to #11902

This PR simplifies the `LinterResult` struct by avoiding the generic and
not store the `ParseError`.

This is possible because the callers already have access to the
`ParseError` via the `Parsed` output. This also means that we can
simplify the return type of `check_path` and avoid the generic `T` on
`LinterResult`.

## Test Plan

`cargo insta test`
2024-06-27 13:44:11 +02:00
Dhruv Manilawala
73851e73ab Avoid displaying syntax error as log message (#11902)
## Summary

Follow-up to #11901 

This PR avoids displaying the syntax errors as log message now that the
`E999` diagnostic cannot be disabled.

For context on why this was added, refer to
https://github.com/astral-sh/ruff/pull/2505. Basically, we would allow
ignoring the syntax error diagnostic because certain syntax feature
weren't supported back then like `match` statement. And, if a user
ignored `E999`, Ruff would give no feedback if the source code contained
any syntax error. So, this log message was a way to indicate to the user
even if `E999` was disabled.

The current state of the parser is such that (a) it matches with the
latest grammar and (b) it's easy to add support for any new syntax.

**Note:** This PR doesn't remove the `DisplayParseError` struct because
it's still being used by the formatter.

## Test Plan

Update existing snapshots from the integration tests.
2024-06-27 13:44:11 +02:00
Dhruv Manilawala
e7b49694a7 Remove E999 as a rule, disallow any disablement methods for syntax error (#11901)
## Summary

This PR updates the way syntax errors are handled throughout the linter.

The main change is that it's now not considered as a rule which involves
the following changes:
* Update `Message` to be an enum with two variants - one for diagnostic
message and the other for syntax error message
* Provide methods on the new message enum to query information required
by downstream usages

This means that the syntax errors cannot be hidden / disabled via any
disablement methods. These are:
1. Configuration via `select`, `ignore`, `per-file-ignores`, and their
`extend-*` variants
	```console
$ cargo run -- check ~/playground/ruff/src/lsp.py --extend-select=E999
--no-preview --no-cache
	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py
--extend-select=E999 --no-preview --no-cache`
warning: Rule `E999` is deprecated and will be removed in a future
release. Syntax errors will always be shown regardless of whether this
rule is selected or not.
/Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but
unused
	  |
	1 | import abc
	  |        ^^^ F401
	2 | from pathlib import Path
	3 | import os
	  |
	  = help: Remove unused import: `abc`
	```
3. Command-line flags via `--select`, `--ignore`, `--per-file-ignores`,
and their `--extend-*` variants
	```console
$ cargo run -- check ~/playground/ruff/src/lsp.py --no-cache
--config=~/playground/ruff/pyproject.toml
	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py
--no-cache --config=/Users/dhruv/playground/ruff/pyproject.toml`
warning: Rule `E999` is deprecated and will be removed in a future
release. Syntax errors will always be shown regardless of whether this
rule is selected or not.
/Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but
unused
	  |
	1 | import abc
	  |        ^^^ F401
	2 | from pathlib import Path
	3 | import os
	  |
	  = help: Remove unused import: `abc`
	```

This also means that the **output format** needs to be updated:
1. The `code`, `noqa_row`, `url` fields in the JSON output is optional
(`null` for syntax errors)
2. Other formats are changed accordingly
For each format, a new test case specific to syntax errors have been
added. Please refer to the snapshot output for the exact format for
syntax error message.

The output of the `--statistics` flag will have a blank entry for syntax
errors:
```
315     F821    [ ] undefined-name
119             [ ] syntax-error
103     F811    [ ] redefined-while-unused
```

The **language server** is updated to consider the syntax errors by
convert them into LSP diagnostic format separately.

### Preview

There are no quick fixes provided to disable syntax errors. This will
automatically work for `ruff-lsp` because the `noqa_row` field will be
`null` in that case.
<img width="772" alt="Screenshot 2024-06-26 at 14 57 08"
src="https://github.com/astral-sh/ruff/assets/67177269/aaac827e-4777-4ac8-8c68-eaf9f2c36774">

Even with `noqa` comment, the syntax error is displayed:
<img width="763" alt="Screenshot 2024-06-26 at 14 59 51"
src="https://github.com/astral-sh/ruff/assets/67177269/ba1afb68-7eaf-4b44-91af-6d93246475e2">

Rule documentation page:
<img width="1371" alt="Screenshot 2024-06-26 at 16 48 07"
src="https://github.com/astral-sh/ruff/assets/67177269/524f01df-d91f-4ac0-86cc-40e76b318b24">


## Test Plan

- [x] Disablement methods via config shows a warning
	- [x] `select`, `extend-select`
	- [ ] ~`ignore`~ _doesn't show any message_
- [ ] ~`per-file-ignores`, `extend-per-file-ignores`~ _doesn't show any
message_
- [x] Disablement methods via command-line flag shows a warning
	- [x] `--select`, `--extend-select`
	- [ ] ~`--ignore`~ _doesn't show any message_
- [ ] ~`--per-file-ignores`, `--extend-per-file-ignores`~ _doesn't show
any message_
- [x] File with syntax errors should exit with code 1
- [x] Language server
	- [x] Should show diagnostics for syntax errors
	- [x] Should not recommend a quick fix edit for adding `noqa` comment
	- [x] Same for `ruff-lsp`

resolves: #8447
2024-06-27 13:44:11 +02:00
Charlie Marsh
c98d8a040f [pyflakes] Stabilize detection of is comparisons to lists, etc. (F632) (#12049)
## Summary

See: https://github.com/astral-sh/ruff/pull/8607. Rare but
uncontroversial.
2024-06-27 13:44:11 +02:00
Charlie Marsh
6f2e024cc6 [flake8-simplify] Stabilize implicit-else simplifications in needless-bool (SIM103) (#12048)
## Summary

See: https://github.com/astral-sh/ruff/pull/10414.

This is a good and intuitive change; we just put it in preview because
it expanded scope a bit.
2024-06-27 13:44:11 +02:00
Charlie Marsh
fb1d7610ac Stabilize allowance of os.environ modifications between imports (#12047)
## Summary

See: https://github.com/astral-sh/ruff/pull/10066.
2024-06-27 13:44:11 +02:00
Alex Waygood
bd845812c7 [Ruff 0.5] Stabilise 11 FURB rules (#12043) 2024-06-27 13:44:11 +02:00
Alex Waygood
c7b2f2b788 [Ruff 0.5] Stabilise manual-dict-comprehension (PERF403) (#12045) 2024-06-27 13:44:11 +02:00
Auguste Lalande
8cc96d7868 Re-code flake8-trio and flake8-async rules to match upstream (#10416)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-06-27 13:44:11 +02:00
Micha Reiser
4b3278fe0b refactor: Compile time enforcement that all top level lint options are checked for deprecation (#12037) 2024-06-27 13:44:11 +02:00
Micha Reiser
41203ea208 Remove output format text and use format full by default (#12010)
Resolves #7349
2024-06-27 13:44:11 +02:00
Alex Waygood
c0d2f439b7 Stabilise django-extra (S610) for release 0.5 (#12029)
The motivation for this rule is solid; it's been in preview for a long
time; the implementation and tests seem sound; there are no open issues
regarding it, and as far as I can tell there never have been any.

The only issue I see is that the docs don't really describe the rule
accurately right now; I fix that in this PR.
2024-06-27 13:44:11 +02:00
Charlie Marsh
b0b68a5601 Migrate release workflow to cargo-dist (#9559)
## Summary

This PR migrates our release workflow to
[`cargo-dist`](https://github.com/axodotdev/cargo-dist). The primary
motivation here is that we want to ship dedicated installers for Ruff
that work across platforms, and `cargo-dist` gives us those installers
out-of-the-box. The secondary motivation is that `cargo-dist` formalizes
some of the patterns that we've built up over time in our own release
process.

At a high level:

- The `release.yml` file is generated by `cargo-dist` with `cargo dist
generate`. It doesn't contain any modifications vis-a-vis the generated
file. (If it's edited out of band from generation, the release fails.)
- Our customizations are inserted as custom steps within the
`cargo-dist` workflow. Specifically, `build-binaries` builds the wheels
and packages them into binaries (as on `main`), while `build-docker.yml`
builds the Docker image. `publish-pypi.yml` publishes the wheels to
PyPI. This is effectively our `release.yaml` (on `main`), broken down
into individual workflows rather than steps within a single workflow.

### Changes from `main`

The workflow is _nearly_ unchanged. We kick off a release manually via
the GitHub Action by providing a tag. If the tag doesn't match the
`Cargo.toml`, the release fails. If the tag matches an already-existing
release, the release fails.

The release proceeds by (in order):

0. Doing some upfront validation via `cargo-dist`.
1. Creating the wheels and archives.
2. Building and pushing the Docker image.
3. Publishing to PyPI (if it's not a "dry run").
4. Creating the GitHub Release (if it's not a "dry run").
5. Notifying `ruff-pre-commit` (if it's not a "dry run").

There are a few changes in the workflow as compared to `main`:

- **We no longer validate the SHA** (just the tag). It's not an input to
the job. The Axo team is considering whether / how to support this.
- **Releases are now published directly** (rather than as draft). Again,
the Axo team is considering whether / how to support this. The downside
of drafts is that the URLs aren't stable, so the installers don't work
_as long as the release is in draft_. This is fine for our workflow. It
seems like the Axo team will add it.
- Releases already contain the latest entry from the changelog (we don't
need to copy it over). This "Just Works", which is nice, though we'll
still want to edit them to add contributors.

There are also a few **breaking changes** for consumers of the binaries:

- **We no longer include the version tag in the file name**. This
enables users to install via `/latest` URLs on GitHub, and is part of
the cargo-dist paradigm.
- **Archives now include an extra level of nesting,** which you can
remove with `--strip-components=1` when untarring.

Here's an example release that I created -- I omitted all the artifacts
since I was just testing a workflow, so none of the installers or links
work, but it gives you a sense for what the release looks like:
https://github.com/charliermarsh/cargodisttest/releases/tag/0.1.13.

### Test Plan

I ran a successful release to completion last night, and installed Ruff
via the installer:

![Screenshot 2024-01-17 at 12 12
53 AM](https://github.com/astral-sh/ruff/assets/1309177/a5334466-2ca3-4279-a453-e912a0805df2)

![Screenshot 2024-01-17 at 12 12
48 AM](https://github.com/astral-sh/ruff/assets/1309177/63ac969e-69a1-488c-8367-4cb783526ca7)

The piece I'm least confident about is the Docker push. We build the
image, but the push fails in my test repo since I haven't wired up the
credentials.
2024-06-27 13:44:11 +02:00
Auguste Lalande
c9a283a5ad [pycodestyle] Remove deprecated functionality from type-comparison (E721) (#11220)
## Summary

Stabilizes `E721` behavior implemented in #7905.

The functionality change in `E721` was implemented in #7905, released in
[v0.1.2](https://github.com/astral-sh/ruff/releases/tag/v0.1.2). And
seems functionally stable since #9676, without an explicit release but
would correspond to
[v0.2.0](https://github.com/astral-sh/ruff/releases/tag/v0.2.0). So the
deprecated functionally should be removable in the next minor release.

resolves: #6465
2024-06-27 13:44:11 +02:00
Alex Waygood
c54bf0c734 Stabilise rules RUF024 and RUF026 (#12026) 2024-06-27 13:44:11 +02:00
Dhruv Manilawala
1968332d93 Redirect PLR1701 to SIM101 (#12021)
## Summary

This rule removes `PLR1701` and redirects it to `SIM101`.

In addition to that, the `SIM101` autofix has been fixed to add padding
if required.

### `PLR1701` has bugs

It also seems that the implementation of `PLR1701` is incorrect in
multiple scenarios. For example, the following code snippet:
```py
# There are two _different_ variables `a` and `b`
if isinstance(a, int) or isinstance(b, bool) or isinstance(a, float):
    pass
# There's another condition `or 1`
if isinstance(self.k, int) or isinstance(self.k, float) or 1:
    pass
```
is fixed to:
```py
# Fixed to only considering variable `a`
if isinstance(a, (float, int)):
    pass
# The additional condition is not present in the fix
if isinstance(self.k, (float, int)):
    pass
```

Playground: https://play.ruff.rs/6cfbdfb7-f183-43b0-b59e-31e728b34190

## Documentation Preview

### `PLR1701`

<img width="1397" alt="Screenshot 2024-06-25 at 11 14 40"
src="https://github.com/astral-sh/ruff/assets/67177269/779ee84d-7c4d-4bb8-a3a4-c2b23a313eba">

## Test Plan

Remove the test cases for `PLR1701`, port the padding test case to
`SIM101` and update the snapshot.
2024-06-27 13:44:11 +02:00
Alex Waygood
0a24d70bfd [Ruff v0.5] Fix ZeroDivisionErrors in the ecosystem check (#12027)
Seen in CI in https://github.com/astral-sh/ruff/pull/12026
2024-06-27 13:44:11 +02:00
Charlie Marsh
a4d711f25f Modify diagnostic ranges for shell-related bandit rules (#10667)
Closes https://github.com/astral-sh/ruff/issues/9994.
2024-06-27 13:44:11 +02:00
Sergey Chudov
c46ae3a3cf Added ignoring deprecated rules for --select=ALL (#10497)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-06-27 13:44:11 +02:00
Micha Reiser
9e8a45f343 Error when using the tab-size option (#12006) 2024-06-27 13:44:11 +02:00
Micha Reiser
36a9efdb48 Remove check, --explain, --clean, --generate-shell-completion aliases (#12011) 2024-06-27 13:44:11 +02:00
T-256
d6a2cad9c2 Drop deprecated nursery rule group (#10172)
Co-authored-by: Micha Reiser <micha@reiser.io>
Resolves https://github.com/astral-sh/ruff/issues/7992
2024-06-27 13:44:11 +02:00
Charlie Marsh
117203f713 Read user configuration from ~/.config/ruff/ruff.toml on macOS (#11115)
Co-authored-by: Micha Reiser <micha@reiser.io>
Closes https://github.com/astral-sh/ruff/issues/10739.
2024-06-27 13:44:11 +02:00
renovate[bot]
12effb897c Update Rust crate unicode-width to v0.1.13 (#11194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-06-27 13:44:11 +02:00
Charlie Marsh
bfe36b9584 Use rule name rather than message in --statistics (#11697)
Co-authored-by: Micha Reiser <micha@reiser.io>
Closes https://github.com/astral-sh/ruff/issues/11097.
2024-06-27 13:44:11 +02:00
Tibor Reiss
b24e4473c5 Remove deprecated configuration '--show-source` (#9814)
Co-authored-by: Micha Reiser <micha@reiser.io>
Fixes parts of https://github.com/astral-sh/ruff/issues/7650
2024-06-27 13:44:11 +02:00
Dhruv Manilawala
a4688aebe9 Use TokenSource to find new location for re-lexing (#12060)
## Summary

This PR splits the re-lexing logic into two parts:
1. `TokenSource`: The token source will be responsible to find the
position the lexer needs to be moved to
2. `Lexer`: The lexer will be responsible to reduce the nesting level
and move itself to the new position if recovered from a parenthesized
context

This split makes it easy to find the new lexer position without needing
to implement the backwards lexing logic again which would need to handle
cases involving:
* Different kinds of newlines
* Line continuation character(s)
* Comments
* Whitespaces

### F-strings

This change did reveal one thing about re-lexing f-strings. Consider the
following example:
```py
f'{'
#  ^
f'foo'
```

Here, the quote as highlighted by the caret (`^`) is the start of a
string inside an f-string expression. This is unterminated string which
means the token emitted is actually `Unknown`. The parser tries to
recover from it but there's no newline token in the vector so the new
logic doesn't recover from it. The previous logic does recover because
it's looking at the raw characters instead.

The parser would be at `FStringStart` (the one for the second line) when
it calls into the re-lexing logic to recover from an unterminated
f-string on the first line. So, moving backwards the first character
encountered is a newline character but the first token encountered is an
`Unknown` token.

This is improved with #12067 

fixes: #12046 
fixes: #12036

## Test Plan

Update the snapshot and validate the changes.
2024-06-27 17:12:39 +05:30
Dhruv Manilawala
e137c824c3 Avoid consuming newline for unterminated string (#12067)
## Summary

This PR fixes the lexer logic to **not** consume the newline character
for an unterminated string literal.

Currently, the lexer would consume it to be part of the string itself
but that would be bad for recovery because then the lexer wouldn't emit
the newline token ever. This PR fixes that to avoid consuming the
newline character in that case.

This was discovered during https://github.com/astral-sh/ruff/pull/12060.

## Test Plan

Update the snapshots and validate them.
2024-06-27 17:02:48 +05:30
baggiponte
55f4812051 docs: add and formatter to CLI startup message (#12042)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-06-26 10:57:10 +00:00
Dhruv Manilawala
47c9ed07f2 Consider 2-character EOL before line continuation (#12035)
## Summary

This PR fixes a bug introduced in
https://github.com/astral-sh/ruff/pull/12008 which didn't consider the
two character newline after the line continuation character.

For example, consider the following code highlighted with whitespaces:
```py
call(foo # comment \\r\n
\r\n
def bar():\r\n
....pass\r\n
```
The lexer is at `def` when it's running the re-lexing logic and trying
to move back to a newline character. It encounters `\n` and it's being
escaped (incorrect) but `\r` is being escaped, so it moves the lexer to
`\n` character. This creates an overlap in token ranges which causes the
panic.

```
Name 0..4
Lpar 4..5
Name 5..8
Comment 9..20
NonLogicalNewline 20..22 <-- overlap between
Newline 21..22           <-- these two tokens
NonLogicalNewline 22..23
Def 23..26
...
```

fixes: #12028 

## Test Plan

Add a test case with line continuation and windows style newline
character.
2024-06-26 14:00:48 +05:30
Dhruv Manilawala
7cb2619ef5 Add syntax error for empty type parameter list (#12030)
## Summary

(I'm pretty sure I added this in the parser re-write but must've got
lost in the rebase?)

This PR raises a syntax error if the type parameter list is empty.

As per the grammar, there should be at least one type parameter:
```
type_params: 
    | invalid_type_params
    | '[' type_param_seq ']' 

type_param_seq: ','.type_param+ [','] 
```

Verified via the builtin `ast` module as well:
```console    
$ python3.13 -m ast parser/_.py
Traceback (most recent call last):
  [..]
  File "parser/_.py", line 1
    def foo[]():
            ^
SyntaxError: Type parameter list cannot be empty
```

## Test Plan

Add inline test cases and update the snapshots.
2024-06-26 08:10:35 +05:30
Charlie Marsh
83fe44728b Match import name ignores against both name and alias (#12033)
## Summary

Right now, it's inconsistent... We sometimes match against the name, and
sometimes against the alias (`asname`). I could see a case for always
matching against the name, but matching against both seems fine too,
since the rule is really about the combination of the two?

Closes https://github.com/astral-sh/ruff/issues/12031.
2024-06-25 18:47:19 -04:00
Alex Waygood
00e456ead4 Fix RUF027 false positives if gettext is imported using an alias (#12025) 2024-06-25 19:10:25 +01:00
Dhruv Manilawala
2853751344 Avoid E203 for f-string debug expression (#12024)
## Summary

This PR fixes a bug where Ruff would raise `E203` for f-string debug
expression. This isn't valid because whitespaces are important for debug
expressions.

fixes: #12023

## Test Plan

Add test case and make sure there are no snapshot changes.
2024-06-25 15:00:31 +05:30
Dhruv Manilawala
7109214b57 Update parser tests to validate token ranges (#12019)
## Summary

This PR updates the parser test infrastructure to validate the token
ranges.

From the code documentation:
```
/// Verifies that:
/// * the ranges are strictly increasing when loop the tokens in insertion order
/// * all ranges are within the length of the source code
```

Follow-up from #12016 and #12017
resolves: #11938

## Test Plan

Make sure that there are no failures.
2024-06-25 08:14:28 +00:00
Dhruv Manilawala
d930e97212 Do not include newline for unterminated string range (#12017)
## Summary

This PR updates the unterminated string error range to not include the
final newline character.

This is a follow-up to #12016 and required for #12019

This is not done for when the unterminated string goes till the end of
file (not a newline character). The unterminated f-string range is
correct.

### Why is this required for #12019 ?

Because otherwise the token ranges will overlap. For example:
```py
f"{"
f"{foo!r"
```

Here, the re-lexing logic recovers from an unterminated f-string and
thus emitting a `Newline` token for the one at the end of the first
line. But, currently the `Unknown` and the `Newline` token would overlap
because the `Unknown` token (unterminated string literal) range would
include the newline character.

## Test Plan

Update and validate the snapshot.
2024-06-25 08:10:07 +00:00
Dhruv Manilawala
9c1b6ec411 Use correct range to highlight line continuation error (#12016)
## Summary

This PR fixes the range highlighted for the line continuation error.

Previously, it would highlight an incorrect range:
```
1 | call(a, b, \\\
  |           ^^ Syntax Error: unexpected character after line continuation character
2 | 
3 | def bar():
  |
```

And now:
```
  |
1 | call(a, b, \\\
  |             ^ Syntax Error: unexpected character after line continuation character
2 | 
3 | def bar():
  |
```

This is implemented by avoiding to update the token range for the
`Unknown` token which is emitted when there's a lexical error. Instead,
the `push_error` helper method will be responsible to update the range
to the error location.

This actually becomes a requirement which can be seen in follow-up PRs.

## Test Plan

Update and validate the snapshot.
2024-06-25 13:35:24 +05:30
Micha Reiser
692309ebd7 [red-knot] Fix tests in release builds (#12022) 2024-06-25 06:34:35 +00:00
Dhruv Manilawala
68a8978454 Consider line continuation character for re-lexing (#12008)
## Summary

This PR fixes a bug where the re-lexing logic didn't consider the line
continuation character being present before the newline character. This
meant that the lexer was being moved back to the newline character which
is actually ignored via `\`.

Considering the following code:
```py
f'middle {'string':\
        'format spec'}

```

The old token stream is:
```
...
Colon 18..19
FStringMiddle 19..29 (flags = F_STRING)
Newline 20..21
Indent 21..29
String 29..42
Rbrace 42..43
...
```

Notice how the ranges are overlapping between the `FStringMiddle` token
and the tokens emitted after moving the lexer backwards.

After this fix, the new token stream which is without moving the lexer
backwards in this scenario:
```
FStringStart 0..2 (flags = F_STRING)
FStringMiddle 2..9 (flags = F_STRING)
Lbrace 9..10
String 10..18
Colon 18..19
FStringMiddle 19..29 (flags = F_STRING)
FStringEnd 29..30 (flags = F_STRING)
Name 30..36
Name 37..41
Unknown 41..44
Newline 44..45
```

fixes: #12004 

## Test Plan

Add test cases and update the snapshots.
2024-06-25 02:13:54 +00:00
Alex Waygood
cd2af3be73 [red-knot] Reduce allocations when normalizing VendoredPaths (#11992) 2024-06-24 13:08:01 +01:00
Micha Reiser
e2e98d005c Fix missing related settings header (#12013) 2024-06-24 12:29:10 +02:00
223 changed files with 6152 additions and 4671 deletions

1
.gitattributes vendored
View File

@@ -8,6 +8,7 @@ crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_eol.py text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr

View File

@@ -1,21 +1,23 @@
name: "[ruff] Release"
# Build ruff on all platforms.
#
# Generates both wheels (for PyPI) and archived binaries (for GitHub releases).
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
name: "Build binaries"
on:
workflow_dispatch:
workflow_call:
inputs:
tag:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
default: ""
plan:
required: true
type: string
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
# 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
- .github/workflows/build-binaries.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -23,6 +25,7 @@ concurrency:
env:
PACKAGE_NAME: ruff
MODULE_NAME: ruff
PYTHON_VERSION: "3.11"
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -31,11 +34,12 @@ env:
jobs:
sdist:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -49,8 +53,8 @@ jobs:
- name: "Test sdist"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
ruff --help
python -m ruff --help
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
- name: "Upload sdist"
uses: actions/upload-artifact@v4
with:
@@ -58,11 +62,12 @@ jobs:
path: dist
macos-x86_64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-12
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -74,11 +79,6 @@ jobs:
with:
target: x86_64
args: --release --locked --out dist
- name: "Test wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -86,23 +86,29 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
TARGET=x86_64-apple-darwin
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-macos-x86_64
name: artifacts-macos-x86_64
path: |
*.tar.gz
*.sha256
macos-aarch64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -126,18 +132,24 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
TARGET=aarch64-apple-darwin
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-aarch64-apple-darwin
name: artifacts-aarch64-apple-darwin
path: |
*.tar.gz
*.sha256
windows:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: windows-latest
strategy:
matrix:
@@ -151,7 +163,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -171,8 +183,8 @@ jobs:
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help
python -m ruff --help
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -181,18 +193,19 @@ jobs:
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.platform.target }}
name: artifacts-${{ matrix.platform.target }}
path: |
*.zip
*.sha256
linux:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -202,7 +215,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -219,27 +232,36 @@ jobs:
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help
python -m ruff --help
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
set -euo pipefail
TARGET=${{ matrix.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.target }}
name: artifacts-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
linux-cross:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -261,11 +283,13 @@ jobs:
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: arm-unknown-linux-musleabihf
arch: arm
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -282,8 +306,8 @@ jobs:
if: matrix.platform.arch != 'ppc64'
name: Test wheel
with:
arch: ${{ matrix.platform.arch }}
distro: ubuntu20.04
arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }}
distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }}
githubToken: ${{ github.token }}
install: |
apt-get update
@@ -298,19 +322,28 @@ jobs:
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
set -euo pipefail
TARGET=${{ matrix.platform.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.platform.target }}
name: artifacts-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
musllinux:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -320,7 +353,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -343,26 +376,35 @@ jobs:
apk add python3
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
set -euo pipefail
TARGET=${{ matrix.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.target }}
name: artifacts-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
musllinux-cross:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -376,7 +418,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -400,204 +442,29 @@ jobs:
run: |
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
set -euo pipefail
TARGET=${{ matrix.platform.target }}
ARCHIVE_NAME=ruff-$TARGET
ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz
mkdir -p $ARCHIVE_NAME
cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.platform.target }}
name: artifacts-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
validate-tag:
name: Validate tag
runs-on: ubuntu-latest
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: main # We checkout the main branch to check for the commit
- 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 tag consistency
run: |
# Switch to the commit we want to release
git checkout ${{ inputs.sha }}
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
upload-release:
name: Upload to PyPI
runs-on: ubuntu-latest
needs:
- macos-aarch64
- macos-x86_64
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
- validate-tag
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true
tag-release:
name: Tag release
runs-on: ubuntu-latest
needs: upload-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For git tag
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: git tag
run: |
git config user.email "hey@astral.sh"
git config user.name "Ruff Release CI"
git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}"
# If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip
# existing), so we make a non-destructive exit here
git push --tags
publish-release:
name: Publish to GitHub
runs-on: ubuntu-latest
needs: tag-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: binaries-*
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v2
with:
draft: true
files: binaries/*
tag_name: v${{ inputs.tag }}
docker-publish:
# This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
# the tag here also
name: Push Docker image ghcr.io/astral-sh/ruff
runs-on: ubuntu-latest
environment:
name: release
permissions:
# For the docker push
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
# Unlike validate-tag we don't check if the commit is on the main branch, but it seems good enough since we can
# change docker tags
if: ${{ inputs.tag }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.tag != '' }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ inputs.tag || 'dry-run' }}
labels: ${{ steps.meta.outputs.labels }}
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Update dependents
runs-on: ubuntu-latest
needs: publish-release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'astral-sh',
repo: 'ruff-pre-commit',
workflow_id: 'main.yml',
ref: 'main',
})

68
.github/workflows/build-docker.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
# Build and publish a Docker image.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
#
# TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but
# sharing the built image as an artifact between jobs is challenging.
name: "[ruff] Build Docker image"
on:
workflow_call:
inputs:
plan:
required: true
type: string
pull_request:
paths:
- .github/workflows/build-docker.yml
jobs:
docker-publish:
name: Build Docker image (ghcr.io/astral-sh/ruff)
runs-on: ubuntu-latest
environment:
name: release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || 'dry-run' }}
labels: ${{ steps.meta.outputs.labels }}

29
.github/workflows/notify-dependents.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# Notify downstream repositories of a new release.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a post-announce
# job within `cargo-dist`.
name: "[ruff] Notify dependents"
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
update-dependents:
name: Notify dependents
runs-on: ubuntu-latest
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'astral-sh',
repo: 'ruff-pre-commit',
workflow_id: 'main.yml',
ref: 'main',
})

34
.github/workflows/publish-pypi.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
# Publish a release to PyPI.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job
# within `cargo-dist`.
name: "[ruff] Publish to PyPI"
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
pypi-publish:
name: Upload to PyPI
runs-on: ubuntu-latest
environment:
name: release
permissions:
# For PyPI's trusted publishing.
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true

249
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,249 @@
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
contents: write
# This task will run whenever you workflow_dispatch with a tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
workflow_dispatch:
inputs:
tag:
description: Release Tag
required: true
default: dry-run
type: string
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: ubuntu-latest
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }}
tag-flag: ${{ inputs.tag && inputs.tag != 'dry-run' && format('--tag={0}', inputs.tag) || '' }}
publishing: ${{ inputs.tag && inputs.tag != 'dry-run' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
custom-build-binaries:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
uses: ./.github/workflows/build-binaries.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
custom-build-docker:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
uses: ./.github/workflows/build-docker.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
permissions:
packages: write
contents: read
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- custom-build-binaries
- custom-build-docker
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- custom-build-binaries
- custom-build-docker
- build-global-artifacts
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.0/cargo-dist-installer.sh | sh"
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
# This is a harmless no-op for GitHub Releases, hosting for that happens in "announce"
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
custom-publish-pypi:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-pypi.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
id-token: write
packages: write
# Create a GitHub Release while uploading all files to it
announce:
needs:
- plan
- host
- custom-publish-pypi
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.plan.outputs.tag }}
name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
artifacts: "artifacts/*"
custom-notify-dependents:
needs:
- plan
- announce
uses: ./.github/workflows/notify-dependents.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Auto-generated by `cargo-dist`.
.github/workflows/release.yml

View File

@@ -1,5 +1,12 @@
# Breaking Changes
## 0.5.0
- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms)
- Selecting `ALL` now excludes deprecated rules
- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring.
- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub.
## 0.3.0
### Ruff 2024.2 style

View File

@@ -1,5 +1,141 @@
# Changelog
## 0.5.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.5.0) for a migration guide and overview of the changes!
### Breaking changes
See also, the "Remapped rules" section which may result in disabled rules.
- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms)
- Selecting `ALL` now excludes deprecated rules
- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring.
- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub.
### Deprecations
The following rules are now deprecated:
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`): Syntax errors are now always shown
### Remapped rules
The following rules have been remapped to new rule codes:
- [`blocking-http-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-in-async-function/): `ASYNC100` to `ASYNC210`
- [`open-sleep-or-subprocess-in-async-function`](https://docs.astral.sh/ruff/rules/open-sleep-or-subprocess-in-async-function/): `ASYNC101` split into `ASYNC220`, `ASYNC221`, `ASYNC230`, and `ASYNC251`
- [`blocking-os-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-os-call-in-async-function/): `ASYNC102` has been merged into `ASYNC220` and `ASYNC221`
- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await/): `TRIO100` to `ASYNC100`
- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call/): `TRIO105` to `ASYNC105`
- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout/): `TRIO109` to `ASYNC109`
- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep/): `TRIO110` to `ASYNC110`
- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call/): `TRIO115` to `ASYNC115`
- [`repeated-isinstance-calls`](https://docs.astral.sh/ruff/rules/repeated-isinstance-calls/): `PLR1701` to `SIM101`
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`mutable-fromkeys-value`](https://docs.astral.sh/ruff/rules/mutable-fromkeys-value/) (`RUF024`)
- [`default-factory-kwarg`](https://docs.astral.sh/ruff/rules/default-factory-kwarg/) (`RUF026`)
- [`django-extra`](https://docs.astral.sh/ruff/rules/django-extra/) (`S610`)
- [`manual-dict-comprehension`](https://docs.astral.sh/ruff/rules/manual-dict-comprehension/) (`PERF403`)
- [`print-empty-string`](https://docs.astral.sh/ruff/rules/print-empty-string/) (`FURB105`)
- [`readlines-in-for`](https://docs.astral.sh/ruff/rules/readlines-in-for/) (`FURB129`)
- [`if-expr-min-max`](https://docs.astral.sh/ruff/rules/if-expr-min-max/) (`FURB136`)
- [`bit-count`](https://docs.astral.sh/ruff/rules/bit-count/) (`FURB161`)
- [`redundant-log-base`](https://docs.astral.sh/ruff/rules/redundant-log-base/) (`FURB163`)
- [`regex-flag-alias`](https://docs.astral.sh/ruff/rules/regex-flag-alias/) (`FURB167`)
- [`isinstance-type-none`](https://docs.astral.sh/ruff/rules/isinstance-type-none/) (`FURB168`)
- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`)
- [`implicit-cwd`](https://docs.astral.sh/ruff/rules/implicit-cwd/) (`FURB177`)
- [`hashlib-digest-hex`](https://docs.astral.sh/ruff/rules/hashlib-digest-hex/) (`FURB181`)
- [`list-reverse-copy`](https://docs.astral.sh/ruff/rules/list-reverse-copy/) (`FURB187`)
- [`bad-open-mode`](https://docs.astral.sh/ruff/rules/bad-open-mode/) (`PLW1501`)
- [`empty-comment`](https://docs.astral.sh/ruff/rules/empty-comment/) (`PLR2044`)
- [`global-at-module-level`](https://docs.astral.sh/ruff/rules/global-at-module-level/) (`PLW0604`)
- [`misplaced-bare-raise`](https://docs.astral.sh/ruff/rules/misplaced-bare-raise%60/) (`PLE0744`)
- [`non-ascii-import-name`](https://docs.astral.sh/ruff/rules/non-ascii-import-name/) (`PLC2403`)
- [`non-ascii-name`](https://docs.astral.sh/ruff/rules/non-ascii-name/) (`PLC2401`)
- [`nonlocal-and-global`](https://docs.astral.sh/ruff/rules/nonlocal-and-global/) (`PLE0115`)
- [`potential-index-error`](https://docs.astral.sh/ruff/rules/potential-index-error/) (`PLE0643`)
- [`redeclared-assigned-name`](https://docs.astral.sh/ruff/rules/redeclared-assigned-name/) (`PLW0128`)
- [`redefined-argument-from-local`](https://docs.astral.sh/ruff/rules/redefined-argument-from-local/) (`PLR1704`)
- [`repeated-keyword-argument`](https://docs.astral.sh/ruff/rules/repeated-keyword-argument/) (`PLE1132`)
- [`super-without-brackets`](https://docs.astral.sh/ruff/rules/super-without-brackets/) (`PLW0245`)
- [`unnecessary-list-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-list-index-lookup/) (`PLR1736`)
- [`useless-exception-statement`](https://docs.astral.sh/ruff/rules/useless-exception-statement/) (`PLW0133`)
- [`useless-with-lock`](https://docs.astral.sh/ruff/rules/useless-with-lock/) (`PLW2101`)
The following behaviors have been stabilized:
- [`is-literal`](https://docs.astral.sh/ruff/rules/is-literal/) (`F632`) now warns for identity checks against list, set or dictionary literals
- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) now detects `if` expressions with implicit `else` branches
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) now allows `os.environ` modifications between import statements
- [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison/) (`E721`) now allows idioms such as `type(x) is int`
- [`yoda-condition`](https://docs.astral.sh/ruff/rules/yoda-conditions/) (`SIM300`) now flags a wider range of expressions
### Removals
The following deprecated settings have been removed:
- `output-format=text`; use `output-format=concise` or `output-format=full`
- `tab-size`; use `indent-width`
The following deprecated CLI options have been removed:
- `--show-source`; use `--output-format=full`
- `--no-show-source`; use `--output-format=concise`
The following deprecated CLI commands have been removed:
- `ruff <path>`; use `ruff check <path>`
- `ruff --clean`; use `ruff clean`
- `ruff --generate-shell-completion`; use `ruff generate-shell-completion`
### Preview features
- \[`ruff`\] Add `assert-with-print-message` rule ([#11981](https://github.com/astral-sh/ruff/pull/11981))
### CLI
- Use rule name rather than message in `--statistics` ([#11697](https://github.com/astral-sh/ruff/pull/11697))
- Use the output format `full` by default ([#12010](https://github.com/astral-sh/ruff/pull/12010))
- Don't log syntax errors to the console ([#11902](https://github.com/astral-sh/ruff/pull/11902))
### Rule changes
- \[`ruff`\] Fix false positives if `gettext` is imported using an alias (`RUF027`) ([#12025](https://github.com/astral-sh/ruff/pull/12025))
- \[`npy`\] Update `trapz` and `in1d` deprecation (`NPY201`) ([#11948](https://github.com/astral-sh/ruff/pull/11948))
- \[`flake8-bandit`\] Modify diagnostic ranges for shell-related rules ([#10667](https://github.com/astral-sh/ruff/pull/10667))
### Server
- Closing an untitled, unsaved notebook document no longer throws an error ([#11942](https://github.com/astral-sh/ruff/pull/11942))
- Support the usage of tildes and environment variables in `logFile` ([#11945](https://github.com/astral-sh/ruff/pull/11945))
- Add option to configure whether to show syntax errors ([#12059](https://github.com/astral-sh/ruff/pull/12059))
### Bug fixes
- \[`pycodestyle`\] Avoid `E203` for f-string debug expression ([#12024](https://github.com/astral-sh/ruff/pull/12024))
- \[`pep8-naming`\] Match import-name ignores against both name and alias (`N812`, `N817`) ([#12033](https://github.com/astral-sh/ruff/pull/12033))
- \[`pyflakes`\] Detect assignments that shadow definitions (`F811`) ([#11961](https://github.com/astral-sh/ruff/pull/11961))
### Parser
- Emit a syntax error for an empty type parameter list ([#12030](https://github.com/astral-sh/ruff/pull/12030))
- Avoid consuming the newline for unterminated strings ([#12067](https://github.com/astral-sh/ruff/pull/12067))
- Do not include the newline in the unterminated string range ([#12017](https://github.com/astral-sh/ruff/pull/12017))
- Use the correct range to highlight line continuation errors ([#12016](https://github.com/astral-sh/ruff/pull/12016))
- Consider 2-character EOL before line continuations ([#12035](https://github.com/astral-sh/ruff/pull/12035))
- Consider line continuation character for re-lexing ([#12008](https://github.com/astral-sh/ruff/pull/12008))
### Other changes
- Upgrade the Unicode table used for measuring the line-length ([#11194](https://github.com/astral-sh/ruff/pull/11194))
- Remove the deprecation error message for the nursery selector ([#10172](https://github.com/astral-sh/ruff/pull/10172))
## 0.4.10
### Parser

View File

@@ -348,7 +348,6 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Merge the PR
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yaml) with:
- The new version number (without starting `v`)
- The commit hash of the merged release pull request on `main`
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
uploaded anything, you can restart after pushing a fix. If you just need to rerun the build,
@@ -360,10 +359,8 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed.
1. Publish the GitHub release
1. Open the draft release in the GitHub release section
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Verify the GitHub release:
1. The Changelog should match the content of `CHANGELOG.md`
1. Append the contributors from the `bump.sh` script
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when

21
Cargo.lock generated
View File

@@ -754,6 +754,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -1974,7 +1985,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.4.10"
version = "0.5.0"
dependencies = [
"anyhow",
"argfile",
@@ -2153,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.4.10"
version = "0.5.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2493,7 +2504,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"dirs 5.0.1",
"etcetera",
"glob",
"globset",
"ignore",
@@ -3248,9 +3259,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode_names2"

View File

@@ -58,9 +58,9 @@ countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "5.5.3" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.8.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
@@ -219,3 +219,60 @@ opt-level = 1
[profile.profiling]
inherits = "release"
debug = 1
# The profile that 'cargo dist' will build with.
[profile.dist]
inherits = "release"
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.14.0"
# CI backends to support
ci = ["github"]
# The installers to generate for each app
installers = ["shell", "powershell"]
# The archive format to use for windows builds (defaults .zip)
windows-archive = ".zip"
# The archive format to use for non-windows builds (defaults .tar.xz)
unix-archive = ".tar.gz"
# Target platforms to build apps for (Rust target-triple syntax)
targets = [
"aarch64-apple-darwin",
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"arm-unknown-linux-musleabihf",
"armv7-unknown-linux-gnueabihf",
"armv7-unknown-linux-musleabihf",
"i686-pc-windows-msvc",
"i686-unknown-linux-gnu",
"i686-unknown-linux-musl",
"powerpc64-unknown-linux-gnu",
"powerpc64le-unknown-linux-gnu",
"s390x-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
]
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
auto-includes = false
# Whether cargo-dist should create a Github Release or use an existing draft
create-release = true
# Publish jobs to run in CI
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
# Local artifacts jobs to run in CI
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi"]
# Announcement jobs to run in CI
post-announce-jobs = ["./notify-dependents"]
# Skip checking whether the specified configuration files are up to date
allow-dirty = ["ci"]
# Whether to install an updater program
install-updater = false

View File

@@ -152,7 +152,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.10
rev: v0.5.0
hooks:
# Run the linter.
- id: ruff
@@ -334,7 +334,6 @@ quality tools, including:
- [flake8-super](https://pypi.org/project/flake8-super/)
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
- [flake8-todos](https://pypi.org/project/flake8-todos/)
- [flake8-trio](https://pypi.org/project/flake8-trio/)
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/astral-sh/ruff/issues/2102))

View File

@@ -16,5 +16,6 @@ jod = "jod" # e.g., `jod-thread`
[default]
extend-ignore-re = [
# Line ignore with trailing "spellchecker:disable-line"
"(?Rm)^.*#\\s*spellchecker:disable-line$"
"(?Rm)^.*#\\s*spellchecker:disable-line$",
"LICENSEs",
]

View File

@@ -271,17 +271,16 @@ impl SourceOrderVisitor<'_> for SemanticIndexer {
let node_key = NodeKey::from_node(expr.into());
let expression_id = self.expressions_by_id.push(node_key);
debug_assert_eq!(
expression_id,
self.flow_graph_builder
.record_expr(self.current_flow_node())
);
let flow_expression_id = self
.flow_graph_builder
.record_expr(self.current_flow_node());
debug_assert_eq!(expression_id, flow_expression_id);
debug_assert_eq!(
expression_id,
self.symbol_table_builder
.record_expression(self.cur_scope())
);
let symbol_expression_id = self
.symbol_table_builder
.record_expression(self.cur_scope());
debug_assert_eq!(expression_id, symbol_expression_id);
self.expressions.insert(node_key, expression_id);

View File

@@ -1,7 +1,7 @@
[package]
name = "ruff"
version = "0.4.10"
publish = false
version = "0.5.0"
publish = true
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use anyhow::bail;
use anyhow::{anyhow, bail};
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser};
use colored::Colorize;
@@ -18,10 +18,10 @@ use ruff_linter::line_width::LineLength;
use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::{
ExtensionPair, FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion,
SerializationFormat, UnsafeFixes,
ExtensionPair, FilePattern, OutputFormat, PatternPrefixPair, PerFileIgnore, PreviewMode,
PythonVersion, UnsafeFixes,
};
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
@@ -78,7 +78,7 @@ impl GlobalConfigArgs {
#[command(
author,
name = "ruff",
about = "Ruff: An extremely fast Python linter.",
about = "Ruff: An extremely fast Python linter and code formatter.",
after_help = "For help with a specific command, see: `ruff help <command>`."
)]
#[command(version)]
@@ -95,7 +95,6 @@ pub enum Command {
/// Run Ruff on the given files or directories (default).
Check(CheckCommand),
/// Explain a rule (or all rules).
#[clap(alias = "--explain")]
#[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))]
Rule {
/// Rule to explain
@@ -125,10 +124,9 @@ pub enum Command {
output_format: HelpFormat,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
Clean,
/// Generate shell completion.
#[clap(alias = "--generate-shell-completion", hide = true)]
#[clap(hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories.
Format(FormatCommand),
@@ -160,13 +158,6 @@ pub struct CheckCommand {
unsafe_fixes: bool,
#[arg(long, overrides_with("unsafe_fixes"), hide = true)]
no_unsafe_fixes: bool,
/// Show violations with source code.
/// Use `--no-show-source` to disable.
/// (Deprecated: use `--output-format=full` or `--output-format=concise` instead of `--show-source` and `--no-show-source`, respectively)
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Show an enumeration of all fixed lint violations.
/// Use `--no-show-fixes` to disable.
#[arg(long, overrides_with("no_show_fixes"))]
@@ -194,7 +185,7 @@ pub struct CheckCommand {
/// The default serialization format is "concise".
/// In preview mode, the default serialization format is "full".
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
pub output_format: Option<SerializationFormat>,
pub output_format: Option<OutputFormat>,
/// Specify file to write the linter output to (default: stdout).
#[arg(short, long, env = "RUFF_OUTPUT_FILE")]
@@ -365,7 +356,6 @@ pub struct CheckCommand {
long,
// Unsupported default-command arguments.
conflicts_with = "diff",
conflicts_with = "show_source",
conflicts_with = "watch",
)]
pub statistics: bool,
@@ -701,11 +691,7 @@ impl CheckCommand {
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
.map(UnsafeFixes::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
output_format: resolve_output_format(
self.output_format,
resolve_bool_arg(self.show_source, self.no_show_source),
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
),
output_format: resolve_output_format(self.output_format)?,
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
extension: self.extension,
};
@@ -933,41 +919,15 @@ The path `{value}` does not point to a configuration file"
}
}
#[allow(deprecated)]
fn resolve_output_format(
output_format: Option<SerializationFormat>,
show_sources: Option<bool>,
preview: bool,
) -> Option<SerializationFormat> {
Some(match (output_format, show_sources) {
(Some(o), None) => o,
(Some(SerializationFormat::Grouped), Some(true)) => {
warn_user!("`--show-source` with `--output-format=grouped` is deprecated, and will not show source files. Use `--output-format=full` to show source information.");
SerializationFormat::Grouped
}
(Some(fmt), Some(true)) => {
warn_user!("The `--show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
fmt
}
(Some(fmt), Some(false)) => {
warn_user!("The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format={fmt}`.");
fmt
}
(None, Some(true)) => {
warn_user!("The `--show-source` argument is deprecated. Use `--output-format=full` instead.");
SerializationFormat::Full
}
(None, Some(false)) => {
warn_user!("The `--no-show-source` argument is deprecated. Use `--output-format=concise` instead.");
SerializationFormat::Concise
}
(None, None) => return None
}).map(|format| match format {
SerializationFormat::Text => {
warn_user!("`--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `{}`.", SerializationFormat::default(preview));
SerializationFormat::default(preview)
},
other => other
})
output_format: Option<OutputFormat>,
) -> anyhow::Result<Option<OutputFormat>> {
if let Some(OutputFormat::Text) = output_format {
Err(anyhow!("`--output-format=text` is no longer supported. Use `--output-format=full` or `--output-format=concise` instead."))
} else {
Ok(output_format)
}
}
/// CLI settings that are distinct from configuration (commands, lists of files,
@@ -1219,7 +1179,7 @@ struct ExplicitConfigOverrides {
fix_only: Option<bool>,
unsafe_fixes: Option<UnsafeFixes>,
force_exclude: Option<bool>,
output_format: Option<SerializationFormat>,
output_format: Option<OutputFormat>,
show_fixes: Option<bool>,
extension: Option<Vec<ExtensionPair>>,
}

View File

@@ -19,7 +19,7 @@ use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::{DiagnosticKind, Fix};
use ruff_linter::message::Message;
use ruff_linter::message::{DiagnosticMessage, Message};
use ruff_linter::{warn_user, VERSION};
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
@@ -333,12 +333,14 @@ impl FileCache {
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
lint.messages
.iter()
.map(|msg| Message {
kind: msg.kind.clone(),
range: msg.range,
fix: msg.fix.clone(),
file: file.clone(),
noqa_offset: msg.noqa_offset,
.map(|msg| {
Message::Diagnostic(DiagnosticMessage {
kind: msg.kind.clone(),
range: msg.range,
fix: msg.fix.clone(),
file: file.clone(),
noqa_offset: msg.noqa_offset,
})
})
.collect()
};
@@ -412,18 +414,19 @@ impl LintCacheData {
notebook_index: Option<NotebookIndex>,
) -> Self {
let source = if let Some(msg) = messages.first() {
msg.file.source_text().to_owned()
msg.source_file().source_text().to_owned()
} else {
String::new() // No messages, no need to keep the source!
};
let messages = messages
.iter()
.filter_map(|message| message.as_diagnostic_message())
.map(|msg| {
// Make sure that all message use the same source file.
assert_eq!(
msg.file,
messages.first().unwrap().file,
&msg.file,
messages.first().unwrap().source_file(),
"message uses a different source file"
);
CacheMessage {
@@ -571,6 +574,7 @@ mod tests {
use test_case::test_case;
use ruff_cache::CACHE_DIR_NAME;
use ruff_linter::message::Message;
use ruff_linter::settings::flags;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_python_ast::PySourceType;
@@ -633,11 +637,7 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics
.messages
.iter()
.any(|m| m.kind.name == "SyntaxError")
{
if diagnostics.messages.iter().any(Message::is_syntax_error) {
parse_errors.push(path.clone());
}
paths.push(path);

View File

@@ -36,7 +36,7 @@ impl<'a> Explanation<'a> {
message_formats: rule.message_formats(),
fix,
explanation: rule.explanation(),
preview: rule.is_preview() || rule.is_nursery(),
preview: rule.is_preview(),
}
}
}
@@ -62,7 +62,7 @@ fn format_rule_text(rule: Rule) -> String {
output.push('\n');
}
if rule.is_preview() || rule.is_nursery() {
if rule.is_preview() {
output.push_str(
r"This rule is in preview and is not stable. The `--preview` flag is required for use.",
);

View File

@@ -9,19 +9,18 @@ use std::path::Path;
use anyhow::{Context, Result};
use colored::Colorize;
use log::{debug, error, warn};
use log::{debug, warn};
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
use ruff_linter::message::{Message, SyntaxErrorMessage};
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::registry::AsRule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{fs, IOError, SyntaxError};
use ruff_linter::{fs, IOError};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
@@ -55,57 +54,61 @@ impl Diagnostics {
path: Option<&Path>,
settings: &LinterSettings,
) -> Self {
let diagnostic = match err {
match err {
// IO errors.
SourceError::Io(_)
| SourceError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
)
if settings.rules.enabled(Rule::IOError) {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
),
source_file,
TextSize::default(),
)],
FxHashMap::default(),
)
} else {
match path {
Some(path) => {
warn!(
"{}{}{} {err}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
None => {
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
}
}
Self::default()
}
}
// Syntax errors.
SourceError::Notebook(
NotebookError::InvalidJson(_)
| NotebookError::InvalidSchema(_)
| NotebookError::InvalidFormat(_),
) => Diagnostic::new(
SyntaxError {
message: err.to_string(),
},
TextRange::default(),
),
};
if settings.rules.enabled(diagnostic.kind.rule()) {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
diagnostic,
dummy,
TextSize::default(),
)],
FxHashMap::default(),
)
} else {
match path {
Some(path) => {
warn!(
"{}{}{} {err}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
None => {
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
}
) => {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::SyntaxError(SyntaxErrorMessage {
message: err.to_string(),
range: TextRange::default(),
file: dummy,
})],
FxHashMap::default(),
)
}
Self::default()
}
}
}
@@ -261,8 +264,8 @@ pub(crate) fn lint_path(
// Lint the file.
let (
LinterResult {
data: messages,
error: parse_error,
messages,
has_syntax_error: has_error,
},
transformed,
fixed,
@@ -331,7 +334,7 @@ pub(crate) fn lint_path(
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
if parse_error.is_none() {
if !has_error {
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
// and writing the diff to stdout, respectively). If a file has diagnostics, we
// need to avoid reading from and writing to the cache in these modes.
@@ -353,13 +356,6 @@ pub(crate) fn lint_path(
}
}
if let Some(error) = parse_error {
error!(
"{}",
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &transformed)
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
} else {
@@ -404,52 +400,66 @@ pub(crate) fn lint_stdin(
};
// Lint the inputs.
let (
LinterResult {
data: messages,
error: parse_error,
},
transformed,
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
fixed,
}) = lint_fix(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,
) {
match fix_mode {
flags::FixMode::Apply => {
// Write the contents to stdout, regardless of whether any errors were fixed.
transformed.write(&mut io::stdout().lock())?;
}
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
let (LinterResult { messages, .. }, transformed, fixed) =
if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
fixed,
}) = lint_fix(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,
) {
match fix_mode {
flags::FixMode::Apply => {
// Write the contents to stdout, regardless of whether any errors were fixed.
transformed.write(&mut io::stdout().lock())?;
}
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
}
}
flags::FixMode::Generate => {}
}
flags::FixMode::Generate => {}
}
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
let transformed = if let Cow::Owned(transformed) = transformed {
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else {
source_kind
};
(result, transformed, fixed)
// If we fail to fix, lint the original source code.
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
&settings.linter,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
}
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
}
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
@@ -459,37 +469,10 @@ pub(crate) fn lint_stdin(
source_type,
ParseSource::None,
);
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
}
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
}
} else {
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
&settings.linter,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
(result, transformed, fixed)
};
if let Some(error) = parse_error {
error!(
"{}",
DisplayParseError::from_source_kind(error, path.map(Path::to_path_buf), &transformed)
);
}
};
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
FxHashMap::from_iter([(

View File

@@ -8,15 +8,15 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use args::{GlobalConfigArgs, ServerCommand};
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::settings::types::OutputFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
@@ -121,7 +121,6 @@ pub fn run(
command,
global_options,
}: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> {
{
let default_panic_hook = std::panic::take_hook();
@@ -145,23 +144,8 @@ pub fn run(
}));
}
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
// support FORCE_COLOR env var
if let Some(force_color) = std::env::var_os("FORCE_COLOR") {
if force_color.len() > 0 {
colored::control::set_override(true);
}
}
set_up_logging(global_options.log_level())?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command {
Command::Version { output_format } => {
commands::version::version(output_format)?;
@@ -351,10 +335,10 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let preview = pyproject_config.settings.linter.preview.is_enabled();
if cli.watch {
if output_format != SerializationFormat::default(preview) {
if output_format != OutputFormat::default() {
warn_user!(
"`--output-format {}` is always used in watch mode.",
SerializationFormat::default(preview)
OutputFormat::default()
);
}

View File

@@ -2,9 +2,11 @@ use std::process::ExitCode;
use clap::{Parser, Subcommand};
use colored::Colorize;
use log::error;
use ruff::args::{Args, Command};
use ruff::{run, ExitStatus};
use ruff_linter::logging::{set_up_logging, LogLevel};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -23,23 +25,33 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
pub fn main() -> ExitCode {
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
// support FORCE_COLOR env var
if let Some(force_color) = std::env::var_os("FORCE_COLOR") {
if force_color.len() > 0 {
colored::control::set_override(true);
}
}
let args = wild::args_os();
let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// We can't use `warn_user` here because logging isn't set up at this point
// and we also don't know if the user runs ruff with quiet.
// Keep the message and pass it to `run` that is responsible for emitting the warning.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) {
let deprecated_alias_error = match args.get(1).and_then(|arg| arg.to_str()) {
// Deprecated aliases that are handled by clap
Some("--explain") => {
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.")
Some("`ruff --explain <RULE>` has been removed. Use `ruff rule <RULE>` instead.")
}
Some("--clean") => {
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
Some("`ruff --clean` has been removed. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
Some("`ruff --generate-shell-completion <SHELL>` has been removed. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
@@ -51,18 +63,26 @@ pub fn main() -> ExitCode {
&& arg != "-V"
&& arg != "--version"
&& arg != "help" => {
{
args.insert(1, "check".into());
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.")
Some("`ruff <path>` has been removed. Use `ruff check <path>` instead.")
}
},
_ => None
};
if let Some(error) = deprecated_alias_error {
#[allow(clippy::print_stderr)]
if set_up_logging(LogLevel::Default).is_ok() {
error!("{}", error);
} else {
eprintln!("{}", error.red().bold());
}
return ExitCode::FAILURE;
}
let args = Args::parse_from(args);
match run(args, deprecated_alias_warning) {
match run(args) {
Ok(code) => code.into(),
Err(err) => {
#[allow(clippy::print_stderr)]

View File

@@ -13,13 +13,13 @@ use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, RdjsonEmitter, SarifEmitter,
TextEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, Message, MessageKind, PylintEmitter,
RdjsonEmitter, SarifEmitter, TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule};
use ruff_linter::registry::Rule;
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
use crate::diagnostics::{Diagnostics, FixMap};
@@ -36,13 +36,14 @@ bitflags! {
}
#[derive(Serialize)]
struct ExpandedStatistics<'a> {
code: SerializeRuleAsCode,
message: &'a str,
struct ExpandedStatistics {
code: Option<SerializeRuleAsCode>,
name: SerializeMessageKindAsTitle,
count: usize,
fixable: bool,
}
#[derive(Copy, Clone)]
struct SerializeRuleAsCode(Rule);
impl Serialize for SerializeRuleAsCode {
@@ -66,8 +67,31 @@ impl From<Rule> for SerializeRuleAsCode {
}
}
struct SerializeMessageKindAsTitle(MessageKind);
impl Serialize for SerializeMessageKindAsTitle {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}
impl Display for SerializeMessageKindAsTitle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
impl From<MessageKind> for SerializeMessageKindAsTitle {
fn from(kind: MessageKind) -> Self {
Self(kind)
}
}
pub(crate) struct Printer {
format: SerializationFormat,
format: OutputFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
@@ -76,7 +100,7 @@ pub(crate) struct Printer {
impl Printer {
pub(crate) const fn new(
format: SerializationFormat,
format: OutputFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
@@ -217,12 +241,13 @@ impl Printer {
}
if !self.flags.intersects(Flags::SHOW_VIOLATIONS) {
#[allow(deprecated)]
if matches!(
self.format,
SerializationFormat::Text
| SerializationFormat::Full
| SerializationFormat::Concise
| SerializationFormat::Grouped
OutputFormat::Text
| OutputFormat::Full
| OutputFormat::Concise
| OutputFormat::Grouped
) {
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() {
@@ -240,24 +265,23 @@ impl Printer {
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
match self.format {
SerializationFormat::Json => {
OutputFormat::Json => {
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Rdjson => {
OutputFormat::Rdjson => {
RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::JsonLines => {
OutputFormat::JsonLines => {
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Junit => {
OutputFormat::Junit => {
JunitEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Concise
| SerializationFormat::Full => {
OutputFormat::Concise | OutputFormat::Full => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.format == SerializationFormat::Full)
.with_show_source(self.format == OutputFormat::Full)
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
@@ -271,7 +295,7 @@ impl Printer {
self.write_summary_text(writer, diagnostics)?;
}
SerializationFormat::Grouped => {
OutputFormat::Grouped => {
GroupedEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
@@ -286,22 +310,23 @@ impl Printer {
}
self.write_summary_text(writer, diagnostics)?;
}
SerializationFormat::Github => {
OutputFormat::Github => {
GithubEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Gitlab => {
OutputFormat::Gitlab => {
GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Pylint => {
OutputFormat::Pylint => {
PylintEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Azure => {
OutputFormat::Azure => {
AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Sarif => {
OutputFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
#[allow(deprecated)]
OutputFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
}
writer.flush()?;
@@ -317,30 +342,23 @@ impl Printer {
let statistics: Vec<ExpandedStatistics> = diagnostics
.messages
.iter()
.map(|message| {
(
message.kind.rule(),
&message.kind.body,
message.fix.is_some(),
)
})
.sorted()
.fold(vec![], |mut acc, (rule, body, fixable)| {
if let Some((prev_rule, _, _, count)) = acc.last_mut() {
if *prev_rule == rule {
.sorted_by_key(|message| (message.rule(), message.fixable()))
.fold(vec![], |mut acc: Vec<(&Message, usize)>, message| {
if let Some((prev_message, count)) = acc.last_mut() {
if prev_message.rule() == message.rule() {
*count += 1;
return acc;
}
}
acc.push((rule, body, fixable, 1));
acc.push((message, 1));
acc
})
.iter()
.map(|(rule, message, fixable, count)| ExpandedStatistics {
code: (*rule).into(),
count: *count,
message,
fixable: *fixable,
.map(|&(message, count)| ExpandedStatistics {
code: message.rule().map(std::convert::Into::into),
name: message.kind().into(),
count,
fixable: message.fixable(),
})
.sorted_by_key(|statistic| Reverse(statistic.count))
.collect();
@@ -350,9 +368,8 @@ impl Printer {
}
match self.format {
SerializationFormat::Text
| SerializationFormat::Full
| SerializationFormat::Concise => {
#[allow(deprecated)]
OutputFormat::Text | OutputFormat::Full | OutputFormat::Concise => {
// Compute the maximum number of digits in the count and code, for all messages,
// to enable pretty-printing.
let count_width = num_digits(
@@ -364,7 +381,12 @@ impl Printer {
);
let code_width = statistics
.iter()
.map(|statistic| statistic.code.to_string().len())
.map(|statistic| {
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.len()
})
.max()
.unwrap();
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
@@ -378,7 +400,11 @@ impl Printer {
writer,
"{:>count_width$}\t{:<code_width$}\t{}{}",
statistic.count.to_string().bold(),
statistic.code.to_string().red().bold(),
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.red()
.bold(),
if any_fixable {
if statistic.fixable {
&fixable
@@ -388,12 +414,12 @@ impl Printer {
} else {
""
},
statistic.message,
statistic.name,
)?;
}
return Ok(());
}
SerializationFormat::Json => {
OutputFormat::Json => {
writeln!(writer, "{}", serde_json::to_string_pretty(&statistics)?)?;
}
_ => {
@@ -528,7 +554,7 @@ impl FixableStatistics {
let mut unapplicable_unsafe = 0;
for message in &diagnostics.messages {
if let Some(fix) = &message.fix {
if let Some(fix) = message.fix() {
if fix.applies(unsafe_fixes.required_applicability()) {
applicable += 1;
} else {

View File

@@ -1,6 +1,6 @@
//! A test suite that ensures deprecated command line options have appropriate warnings / behaviors
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::settings::types::OutputFormat;
use std::process::Command;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
@@ -9,142 +9,28 @@ const BIN_NAME: &str = "ruff";
const STDIN: &str = "l = 1";
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
fn ruff_check(output_format: OutputFormat) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
let output_format = output_format.to_string();
cmd.arg("check")
.arg("--output-format")
.arg(output_format)
.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");
}
Some(false) => {
cmd.arg("--no-show-source");
}
None => {}
}
cmd.arg("-");
cmd
}
#[test]
fn ensure_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(true), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_no_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(false), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
#[allow(deprecated)]
fn ensure_output_format_is_deprecated() {
assert_cmd_snapshot!(ruff_check(None, Some("text".into())).pass_stdin(STDIN), @r###"
assert_cmd_snapshot!(ruff_check(OutputFormat::Text).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_full_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("full".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
|
1 | l = 1
| ^ E741
|
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=full`.
"###);
}
#[test]
fn ensure_output_format_uses_concise_over_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
ruff failed
Cause: `--output-format=text` is no longer supported. Use `--output-format=full` or `--output-format=concise` instead.
"###);
}

View File

@@ -812,14 +812,13 @@ tab-size = 2
if True:
pass
"), @r###"
success: true
exit_code: 0
success: false
exit_code: 2
----- stdout -----
if True:
pass
----- stderr -----
warning: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update your configuration to use `indent-width = <value>` instead.
ruff failed
Cause: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update `[RUFF-TOML-PATH]` to use `indent-width = <value>` instead.
"###);
});
Ok(())

View File

@@ -116,6 +116,12 @@ fn stdin_error() {
exit_code: 1
----- stdout -----
-:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -134,6 +140,12 @@ fn stdin_filename() {
exit_code: 1
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -163,7 +175,19 @@ import bar # unused import
exit_code: 1
----- stdout -----
bar.py:2:8: F401 [*] `bar` imported but unused
|
2 | import bar # unused import
| ^^^ F401
|
= help: Remove unused import: `bar`
foo.py:2:8: F401 [*] `foo` imported but unused
|
2 | import foo # unused import
| ^^^ F401
|
= help: Remove unused import: `foo`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -185,6 +209,12 @@ fn check_warn_stdin_filename_with_files() {
exit_code: 1
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -205,6 +235,12 @@ fn stdin_source_type_py() {
exit_code: 1
----- stdout -----
TCH.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -436,6 +472,11 @@ fn stdin_fix_jupyter() {
}
----- stderr -----
Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
|
1 | print(x)
| ^ F821
|
Found 3 errors (2 fixed, 1 remaining).
"###);
}
@@ -529,7 +570,19 @@ fn stdin_override_parser_ipynb() {
exit_code: 1
----- stdout -----
Jupyter.py:cell 1:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Jupyter.py:cell 3:1:8: F401 [*] `sys` imported but unused
|
1 | import sys
| ^^^ F401
|
= help: Remove unused import: `sys`
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -553,6 +606,12 @@ fn stdin_override_parser_py() {
exit_code: 1
----- stdout -----
F401.ipynb:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -575,6 +634,14 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
----- stderr -----
-:3:4: F634 If test is a tuple, which is always `True`
|
1 | import sys
2 |
3 | if (1, 2):
| ^^^^^^ F634
4 | print(sys.version)
|
Found 2 errors (1 fixed, 1 remaining).
"###);
}
@@ -731,11 +798,15 @@ fn stdin_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
|
Found 1 error.
----- stderr -----
error: Failed to parse at 1:16: Expected one or more symbol names after import
"###);
}
@@ -747,12 +818,65 @@ fn stdin_multiple_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
-:2:6: E999 SyntaxError: Expected an expression
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
2 | bar =
|
-:2:6: SyntaxError: Expected an expression
|
1 | from foo import
2 | bar =
| ^
|
Found 2 errors.
----- stderr -----
error: Failed to parse at 1:16: Expected one or more symbol names after import
"###);
}
#[test]
fn parse_error_not_included() {
// Select any rule except for `E999`, syntax error should still be shown.
let mut cmd = RuffCheck::default().args(["--select=I"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^
|
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn deprecated_parse_error_selection() {
let mut cmd = RuffCheck::default().args(["--select=E999"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^
|
Found 1 error.
----- stderr -----
warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not.
"###);
}
@@ -854,117 +978,41 @@ fn show_statistics() {
success: false
exit_code: 1
----- stdout -----
1 F401 [*] `sys` imported but unused
1 F401 [*] unused-import
----- stderr -----
"###);
}
#[test]
fn nursery_prefix() {
// Should only detect RUF90X, but not the unstable test rules
fn show_statistics_json() {
let mut cmd = RuffCheck::default()
.args(["--select", "RUF9", "--output-format=concise"])
.args([
"--select",
"F401",
"--statistics",
"--output-format",
"json",
])
.build();
assert_cmd_snapshot!(cmd, @r###"
assert_cmd_snapshot!(cmd
.pass_stdin("import sys\nimport os\n\nprint(os.getuid())\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF920 Hey this is a deprecated test rule.
-:1:1: RUF921 Hey this is another deprecated test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
[
{
"code": "F401",
"name": "unused-import",
"count": 1,
"fixable": true
}
]
----- stderr -----
"###);
}
#[test]
fn nursery_all() {
// Should detect RUF90X, but not the unstable test rules
let mut cmd = RuffCheck::default()
.args(["--select", "ALL", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: D100 Missing docstring in public module
-:1:1: RUF900 Hey this is a stable test rule.
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF920 Hey this is a deprecated test rule.
-:1:1: RUF921 Hey this is another deprecated test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 8 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- 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 nursery_direct() {
// Should fail when a nursery rule is selected without the preview flag
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "RUF912", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Selection of unstable rule `RUF912` without the `--preview` flag is not allowed.
"###);
}
#[test]
fn nursery_group_selector() {
// The NURSERY selector is removed but parses in the CLI for a nicer error message
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "NURSERY", "--output-format=concise"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The `NURSERY` selector was removed. Use the `--preview` flag instead.
"###);
}
#[test]
fn nursery_group_selector_preview_enabled() {
// When preview mode is enabled, we shouldn't suggest using the `--preview` flag.
// Before Ruff v0.2.0 this would warn
let mut cmd = RuffCheck::default()
.args(["--select", "NURSERY", "--preview"])
.build();
assert_cmd_snapshot!(cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The `NURSERY` selector was removed. Unstable rules should be selected individually or by their respective groups.
"###);
}
#[test]
fn preview_enabled_prefix() {
// All the RUF9XX test rules should be triggered
@@ -980,9 +1028,8 @@ fn preview_enabled_prefix() {
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 7 errors.
Found 6 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1005,9 +1052,8 @@ fn preview_enabled_all() {
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 9 errors.
Found 8 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1145,9 +1191,8 @@ fn preview_enabled_group_ignore() {
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
-:1:1: RUF911 Hey this is a preview test rule.
-:1:1: RUF912 Hey this is a nursery test rule.
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
Found 7 errors.
Found 6 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
@@ -1214,6 +1259,9 @@ fn redirect_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
|
Found 1 error.
----- stderr -----
@@ -1246,6 +1294,9 @@ fn redirect_prefix() {
exit_code: 1
----- stdout -----
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
|
Found 1 error.
----- stderr -----
@@ -1263,6 +1314,9 @@ fn deprecated_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
Found 1 error.
----- stderr -----
@@ -1280,7 +1334,13 @@ fn deprecated_multiple_direct() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
|
Found 2 errors.
----- stderr -----
@@ -1299,7 +1359,13 @@ fn deprecated_indirect() {
exit_code: 1
----- stdout -----
-:1:1: RUF920 Hey this is a deprecated test rule.
|
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
|
Found 2 errors.
----- stderr -----
@@ -1377,7 +1443,8 @@ fn unreadable_pyproject_toml() -> Result<()> {
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args, None).err().context("Unexpected success")?;
let err = run(args).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)
@@ -1450,6 +1517,12 @@ fn check_input_from_argfile() -> Result<()> {
exit_code: 1
----- stdout -----
/path/to/a.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
Found 1 error.
[*] 1 fixable with the `--fix` option.
@@ -1471,7 +1544,13 @@ fn check_hints_hidden_unsafe_fixes() {
exit_code: 1
----- stdout -----
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1489,6 +1568,11 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
exit_code: 1
----- stdout -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
|
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1507,7 +1591,13 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
exit_code: 1
----- stdout -----
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
[*] 1 fixable with the --fix option.
@@ -1527,6 +1617,11 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
exit_code: 1
----- stdout -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
|
Found 1 error.
----- stderr -----
@@ -1544,7 +1639,13 @@ fn check_shows_unsafe_fixes_with_opt_in() {
exit_code: 1
----- stdout -----
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
[*] 2 fixable with the --fix option.
@@ -1566,6 +1667,11 @@ fn fix_applies_safe_fixes_by_default() {
----- stderr -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
1 | # fix from stable-test-rule-safe-fix
| RUF902
|
Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
@@ -1603,6 +1709,11 @@ fn fix_does_not_apply_display_only_fixes() {
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | def add_to_list(item, some_list=[]): ...
| RUF903
|
Found 1 error.
"###);
}
@@ -1621,6 +1732,11 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
def add_to_list(item, some_list=[]): ...
----- stderr -----
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | def add_to_list(item, some_list=[]): ...
| RUF903
|
Found 1 error.
"###);
}
@@ -1638,6 +1754,9 @@ fn fix_only_unsafe_fixes_available() {
----- stderr -----
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
"###);
@@ -1774,7 +1893,13 @@ extend-unsafe-fixes = ["RUF901"]
exit_code: 1
----- stdout -----
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
@@ -1806,7 +1931,13 @@ extend-safe-fixes = ["RUF902"]
exit_code: 1
----- stdout -----
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
[*] 2 fixable with the `--fix` option.
@@ -1840,7 +1971,13 @@ extend-safe-fixes = ["RUF902"]
exit_code: 1
----- stdout -----
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
|
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
|
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1876,12 +2013,61 @@ extend-safe-fixes = ["RUF9"]
exit_code: 1
----- stdout -----
-:1:1: RUF900 Hey this is a stable test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF900
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF901
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF902
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
1 | x = {'a': 1, 'a': 1}
| RUF903
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF920 Hey this is a deprecated test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF920
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF921 Hey this is another deprecated test rule.
|
1 | x = {'a': 1, 'a': 1}
| RUF921
2 | print(('foo'))
3 | print(str('foo'))
|
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
1 | x = {'a': 1, 'a': 1}
| RUF950
2 | print(('foo'))
3 | print(str('foo'))
|
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -1943,6 +2129,12 @@ def log(x, base) -> float:
exit_code: 1
----- stdout -----
-:2:5: D417 Missing argument description in the docstring for `log`: `base`
|
2 | def log(x, base) -> float:
| ^^^ D417
3 | """Calculate natural log of a value
|
Found 1 error.
----- stderr -----
@@ -1973,6 +2165,14 @@ select = ["RUF017"]
exit_code: 1
----- stdout -----
-:3:1: RUF017 Avoid quadratic list summation
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
| ^^^^^^^^^^^^^^^ RUF017
|
= help: Replace with `functools.reduce`
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
@@ -2005,6 +2205,14 @@ unfixable = ["RUF"]
exit_code: 1
----- stdout -----
-:3:1: RUF017 Avoid quadratic list summation
|
1 | x = [1, 2, 3]
2 | y = [4, 5, 6]
3 | sum([x, y], [])
| ^^^^^^^^^^^^^^^ RUF017
|
= help: Replace with `functools.reduce`
Found 1 error.
----- stderr -----

View File

@@ -17,7 +17,7 @@ Settings path: "[BASEPATH]/pyproject.toml"
cache_dir = "[BASEPATH]/.ruff_cache"
fix = false
fix_only = false
output_format = concise
output_format = full
show_fixes = false
unsafe_fixes = hint

View File

@@ -73,7 +73,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
);
// Assert that file contains no parse errors
assert_eq!(result.error, None);
assert!(!result.has_syntax_error);
},
criterion::BatchSize::SmallInput,
);

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::{self, Debug};
@@ -30,7 +31,7 @@ impl VendoredFileSystem {
}
pub fn exists(&self, path: &VendoredPath) -> bool {
let normalized = normalize_vendored_path(path);
let normalized = NormalizedVendoredPath::from(path);
let inner_locked = self.inner.lock();
let mut archive = inner_locked.borrow_mut();
@@ -45,7 +46,7 @@ impl VendoredFileSystem {
}
pub fn metadata(&self, path: &VendoredPath) -> Option<Metadata> {
let normalized = normalize_vendored_path(path);
let normalized = NormalizedVendoredPath::from(path);
let inner_locked = self.inner.lock();
// Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
@@ -72,7 +73,7 @@ impl VendoredFileSystem {
pub fn read(&self, path: &VendoredPath) -> Result<String> {
let inner_locked = self.inner.lock();
let mut archive = inner_locked.borrow_mut();
let mut zip_file = archive.lookup_path(&normalize_vendored_path(path))?;
let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?;
let mut buffer = String::new();
zip_file.read_to_string(&mut buffer)?;
Ok(buffer)
@@ -240,45 +241,76 @@ impl VendoredZipArchive {
/// but trailing slashes are crucial for distinguishing between
/// files and directories inside zip archives.
#[derive(Debug, Clone, PartialEq, Eq)]
struct NormalizedVendoredPath(String);
struct NormalizedVendoredPath<'a>(Cow<'a, str>);
impl NormalizedVendoredPath {
fn with_trailing_slash(mut self) -> Self {
impl<'a> NormalizedVendoredPath<'a> {
fn with_trailing_slash(self) -> Self {
debug_assert!(!self.0.ends_with('/'));
self.0.push('/');
self
let mut data = self.0.into_owned();
data.push('/');
Self(Cow::Owned(data))
}
fn as_str(&self) -> &str {
self.0.as_str()
&self.0
}
}
/// Normalizes the path by removing `.` and `..` components.
///
/// ## Panics:
/// If a path with an unsupported component for vendored paths is passed.
/// Unsupported components are path prefixes,
/// and path root directories appearing anywhere except at the start of the path.
fn normalize_vendored_path(path: &VendoredPath) -> NormalizedVendoredPath {
// Allow the `RootDir` component, but only if it is at the very start of the string.
let mut components = path.components().peekable();
if let Some(camino::Utf8Component::RootDir) = components.peek() {
components.next();
}
let mut normalized_parts = Vec::new();
for component in components {
match component {
camino::Utf8Component::Normal(part) => normalized_parts.push(part),
camino::Utf8Component::CurDir => continue,
camino::Utf8Component::ParentDir => {
normalized_parts.pop();
impl<'a> From<&'a VendoredPath> for NormalizedVendoredPath<'a> {
/// Normalize the path.
///
/// The normalizations are:
/// - Remove `.` and `..` components
/// - Strip trailing slashes
/// - Normalize `\\` separators to `/`
/// - Validate that the path does not have any unsupported components
///
/// ## Panics:
/// If a path with an unsupported component for vendored paths is passed.
/// Unsupported components are path prefixes and path root directories.
fn from(path: &'a VendoredPath) -> Self {
/// Remove `.` and `..` components, and validate that unsupported components are not present.
///
/// This inner routine also strips trailing slashes,
/// and normalizes paths to use Unix `/` separators.
/// However, it always allocates, so avoid calling it if possible.
/// In most cases, the path should already be normalized.
fn normalize_unnormalized_path(path: &VendoredPath) -> String {
let mut normalized_parts = Vec::new();
for component in path.components() {
match component {
camino::Utf8Component::Normal(part) => normalized_parts.push(part),
camino::Utf8Component::CurDir => continue,
camino::Utf8Component::ParentDir => {
// `VendoredPath("")`, `VendoredPath("..")` and `VendoredPath("../..")`
// all resolve to the same path relative to the zip archive
// (see https://github.com/astral-sh/ruff/pull/11991#issuecomment-2185278014)
normalized_parts.pop();
}
unsupported => {
panic!("Unsupported component in a vendored path: {unsupported}")
}
}
}
unsupported => panic!("Unsupported component in a vendored path: {unsupported}"),
normalized_parts.join("/")
}
let path_str = path.as_str();
if std::path::MAIN_SEPARATOR == '\\' && path_str.contains('\\') {
// Normalize paths so that they always use Unix path separators
NormalizedVendoredPath(Cow::Owned(normalize_unnormalized_path(path)))
} else if !path
.components()
.all(|component| matches!(component, camino::Utf8Component::Normal(_)))
{
// Remove non-`Normal` components
NormalizedVendoredPath(Cow::Owned(normalize_unnormalized_path(path)))
} else {
// Strip trailing slashes from the path
NormalizedVendoredPath(Cow::Borrowed(path_str.trim_end_matches('/')))
}
}
NormalizedVendoredPath(normalized_parts.join("/"))
}
#[cfg(test)]

View File

@@ -64,7 +64,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
output.push('\n');
}
if rule.is_preview() || rule.is_nursery() {
if rule.is_preview() {
output.push_str(
r"This rule is unstable and in [preview](../preview.md). The `--preview` flag is required for use.",
);

View File

@@ -34,7 +34,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
format!("<span title='Rule has been deprecated'>{WARNING_SYMBOL}</span>")
}
#[allow(deprecated)]
RuleGroup::Preview | RuleGroup::Nursery => {
RuleGroup::Preview => {
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
}
RuleGroup::Stable => {
@@ -165,9 +165,9 @@ pub(crate) fn generate() -> String {
table_out.push('\n');
}
if Options::metadata().has(linter.name()) {
if Options::metadata().has(&format!("lint.{}", linter.name())) {
table_out.push_str(&format!(
"For related settings, see [{}](settings.md#{}).",
"For related settings, see [{}](settings.md#lint{}).",
linter.name(),
linter.name(),
));

View File

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

View File

@@ -1,23 +1,27 @@
import urllib.request
import requests
import httpx
import trio
async def foo():
urllib.request.urlopen("http://example.com/foo/bar").read()
async def func():
with trio.fail_after():
...
async def foo():
requests.get()
async def func():
with trio.fail_at():
await ...
async def foo():
httpx.get()
async def func():
with trio.move_on_after():
...
async def foo():
requests.post()
async def func():
with trio.move_at():
await ...
async def foo():
httpx.post()
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -1,13 +0,0 @@
import os
async def foo():
os.popen()
async def foo():
os.spawnl()
async def foo():
os.fspath("foo")

View File

@@ -26,7 +26,7 @@ async def func() -> None:
await trio.lowlevel.wait_task_rescheduled(foo)
await trio.lowlevel.wait_writable(foo)
# TRIO105
# ASYNC105
trio.aclose_forcefully(foo)
trio.open_file(foo)
trio.open_ssl_over_tcp_listeners(foo, foo)
@@ -55,10 +55,10 @@ async def func() -> None:
async with await trio.open_file(foo): # Ok
pass
async with trio.open_file(foo): # TRIO105
async with trio.open_file(foo): # ASYNC105
pass
def func() -> None:
# TRIO105 (without fix)
# ASYNC105 (without fix)
trio.open_file(foo)

View File

@@ -2,19 +2,19 @@ async def func():
import trio
from trio import sleep
await trio.sleep(0) # TRIO115
await trio.sleep(0) # ASYNC115
await trio.sleep(1) # OK
await trio.sleep(0, 1) # OK
await trio.sleep(...) # OK
await trio.sleep() # OK
trio.sleep(0) # TRIO115
trio.sleep(0) # ASYNC115
foo = 0
trio.sleep(foo) # OK
trio.sleep(1) # OK
time.sleep(0) # OK
sleep(0) # TRIO115
sleep(0) # ASYNC115
bar = "bar"
trio.sleep(bar)
@@ -45,18 +45,18 @@ async def func():
def func():
import trio
trio.run(trio.sleep(0)) # TRIO115
trio.run(trio.sleep(0)) # ASYNC115
from trio import Event, sleep
def func():
sleep(0) # TRIO115
sleep(0) # ASYNC115
async def func():
await sleep(seconds=0) # TRIO115
await sleep(seconds=0) # ASYNC115
def func():

View File

@@ -0,0 +1,69 @@
import urllib
import requests
import httpx
import urllib3
async def foo():
urllib.request.urlopen("http://example.com/foo/bar").read() # ASYNC210
async def foo():
requests.get() # ASYNC210
async def foo():
httpx.get() # ASYNC210
async def foo():
requests.post() # ASYNC210
async def foo():
httpx.post() # ASYNC210
async def foo():
requests.get() # ASYNC210
requests.get(...) # ASYNC210
requests.get # Ok
print(requests.get()) # ASYNC210
print(requests.get(requests.get())) # ASYNC210
requests.options() # ASYNC210
requests.head() # ASYNC210
requests.post() # ASYNC210
requests.put() # ASYNC210
requests.patch() # ASYNC210
requests.delete() # ASYNC210
requests.foo()
httpx.options("") # ASYNC210
httpx.head("") # ASYNC210
httpx.post("") # ASYNC210
httpx.put("") # ASYNC210
httpx.patch("") # ASYNC210
httpx.delete("") # ASYNC210
httpx.foo() # Ok
urllib3.request() # ASYNC210
urllib3.request(...) # ASYNC210
urllib.request.urlopen("") # ASYNC210
r = {}
r.get("not a sync http client") # Ok
async def bar():
def request():
pass
request() # Ok
def urlopen():
pass
urlopen() # Ok

View File

@@ -0,0 +1,98 @@
import os
import subprocess
# Violation cases:
async def func():
subprocess.run("foo") # ASYNC221
async def func():
subprocess.call("foo") # ASYNC221
async def func():
subprocess.foo(0) # OK
async def func():
os.wait4(10) # ASYNC222
async def func():
os.wait(12) # ASYNC222
async def foo():
await async_fun(
subprocess.getoutput() # ASYNC221
)
subprocess.Popen() # ASYNC220
os.system() # ASYNC221
system()
os.system.anything()
os.anything()
subprocess.run() # ASYNC221
subprocess.call() # ASYNC221
subprocess.check_call() # ASYNC221
subprocess.check_output() # ASYNC221
subprocess.getoutput() # ASYNC221
subprocess.getstatusoutput() # ASYNC221
await async_fun(
subprocess.getoutput() # ASYNC221
)
subprocess.anything()
subprocess.foo()
subprocess.bar.foo()
subprocess()
os.posix_spawn() # ASYNC221
os.posix_spawnp() # ASYNC221
os.spawn()
os.spawn
os.spawnllll()
os.spawnl() # ASYNC221
os.spawnle() # ASYNC221
os.spawnlp() # ASYNC221
os.spawnlpe() # ASYNC221
os.spawnv() # ASYNC221
os.spawnve() # ASYNC221
os.spawnvp() # ASYNC221
os.spawnvpe() # ASYNC221
P_NOWAIT = os.P_NOWAIT
# if mode is given, and is not os.P_WAIT: ASYNC220
os.spawnl(os.P_NOWAIT) # ASYNC220
os.spawnl(P_NOWAIT) # ASYNC220
os.spawnl(mode=os.P_NOWAIT) # ASYNC220
os.spawnl(mode=P_NOWAIT) # ASYNC220
P_WAIT = os.P_WAIT
# if it is P_WAIT, ASYNC221
os.spawnl(P_WAIT) # ASYNC221
os.spawnl(mode=os.P_WAIT) # ASYNC221
os.spawnl(mode=P_WAIT) # ASYNC221
# other weird cases: ASYNC220
os.spawnl(0) # ASYNC220
os.spawnl(1) # ASYNC220
os.spawnl(foo()) # ASYNC220
# ASYNC222
os.wait() # ASYNC222
os.wait3() # ASYNC222
os.wait4() # ASYNC222
os.waitid() # ASYNC222
os.waitpid() # ASYNC222
os.waitpi()
os.waiti()

View File

@@ -1,53 +1,48 @@
import os
import subprocess
import time
import io
from pathlib import Path
async def foo():
open("") # ASYNC230
io.open_code("") # ASYNC230
with open(""): # ASYNC230
...
with open("") as f: # ASYNC230
...
with foo(), open(""): # ASYNC230
...
async with open(""): # ASYNC230
...
def foo_sync():
open("")
# Violation cases:
async def func():
open("foo")
async def func():
time.sleep(1)
async def func():
subprocess.run("foo")
async def func():
subprocess.call("foo")
async def func():
subprocess.foo(0)
async def func():
os.wait4(10)
async def func():
os.wait(12)
open("foo") # ASYNC230
# Violation cases for pathlib:
async def func():
Path("foo").open() # ASYNC101
Path("foo").open() # ASYNC230
async def func():
p = Path("foo")
p.open() # ASYNC101
p.open() # ASYNC230
async def func():
with Path("foo").open() as f: # ASYNC101
with Path("foo").open() as f: # ASYNC230
pass
@@ -55,13 +50,13 @@ async def func() -> None:
p = Path("foo")
async def bar():
p.open() # ASYNC101
p.open() # ASYNC230
async def func() -> None:
(p1, p2) = (Path("foo"), Path("bar"))
p1.open() # ASYNC101
p1.open() # ASYNC230
# Non-violation cases for pathlib:

View File

@@ -0,0 +1,14 @@
import time
import asyncio
async def func():
time.sleep(1) # ASYNC251
def func():
time.sleep(1) # OK
async def func():
asyncio.sleep(1) # OK

View File

@@ -510,7 +510,7 @@ image[:,]
image[:,:,]
lambda x, :
lambda x, : x
# ==> unpack.py <==
def function(

View File

@@ -48,3 +48,7 @@ def f():
if isinstance(a, int) or isinstance(a, float):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
if(isinstance(a, int)) or (isinstance(a, float)):
pass

View File

@@ -1,27 +0,0 @@
import trio
async def func():
with trio.fail_after():
...
async def func():
with trio.fail_at():
await ...
async def func():
with trio.move_on_after():
...
async def func():
with trio.move_at():
await ...
async def func():
with trio.move_at():
async with trio.open_nursery() as nursery:
...

View File

@@ -185,3 +185,7 @@ f"{ham[lower +1 :, "columnname"]}"
#: E203:1:13
f"{ham[lower + 1 :, "columnname"]}"
#: Okay: https://github.com/astral-sh/ruff/issues/12023
f"{x = :.2f}"
f"{(x) = :.2f}"

View File

@@ -1,4 +0,0 @@
def x():

View File

@@ -1,43 +0,0 @@
"""Checks use of consider-merging-isinstance"""
# pylint:disable=line-too-long, simplifiable-condition
def isinstances():
"Examples of isinstances"
var = range(10)
# merged
if isinstance(var[1], (int, float)):
pass
result = isinstance(var[2], (int, float))
# not merged
if isinstance(var[3], int) or isinstance(var[3], float) or isinstance(var[3], list) and True: # [consider-merging-isinstance]
pass
result = isinstance(var[4], int) or isinstance(var[4], float) or isinstance(var[5], list) and False # [consider-merging-isinstance]
result = isinstance(var[5], int) or True or isinstance(var[5], float) # [consider-merging-isinstance]
inferred_isinstance = isinstance
result = inferred_isinstance(var[6], int) or inferred_isinstance(var[6], float) or inferred_isinstance(var[6], list) and False # [consider-merging-isinstance]
result = isinstance(var[10], str) or isinstance(var[10], int) and var[8] * 14 or isinstance(var[10], float) and var[5] * 14.4 or isinstance(var[10], list) # [consider-merging-isinstance]
result = isinstance(var[11], int) or isinstance(var[11], int) or isinstance(var[11], float) # [consider-merging-isinstance]
result = isinstance(var[20])
result = isinstance()
# Combination merged and not merged
result = isinstance(var[12], (int, float)) or isinstance(var[12], list) # [consider-merging-isinstance]
# not merged but valid
result = isinstance(var[5], int) and var[5] * 14 or isinstance(var[5], float) and var[5] * 14.4
result = isinstance(var[7], int) or not isinstance(var[7], float)
result = isinstance(var[6], int) or isinstance(var[7], float)
result = isinstance(var[6], int) or isinstance(var[7], int)
result = isinstance(var[6], (float, int)) or False
return result
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
if(isinstance(self.k, int)) or (isinstance(self.k, float)):
...

View File

@@ -110,7 +110,7 @@ def func():
def func():
defaultdict(dict, defaultdict=list) # OK
defaultdict(dict, default_factory=list) # OK
def func():

View File

@@ -38,3 +38,10 @@ def negative_cases():
print(("{a}" "{c}").format(a=1, c=2))
print("{a}".attribute.chaining.call(a=2))
print("{a} {c}".format(a))
from gettext import gettext as foo
should = 42
x = foo("This {should} also be understood as a translation string")
import django.utils.translations
y = django.utils.translations.gettext("This {should} be understood as a translation string too!")

View File

@@ -15,8 +15,8 @@ use crate::rules::{
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
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_trio, flake8_type_checking, flake8_use_pathlib,
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -505,11 +505,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BlockingHttpCallInAsyncFunction) {
flake8_async::rules::blocking_http_call(checker, call);
}
if checker.enabled(Rule::OpenSleepOrSubprocessInAsyncFunction) {
flake8_async::rules::open_sleep_or_subprocess_call(checker, call);
if checker.enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
if checker.enabled(Rule::BlockingOsCallInAsyncFunction) {
flake8_async::rules::blocking_os_call(checker, call);
if checker.any_enabled(&[
Rule::CreateSubprocessInAsyncFunction,
Rule::RunProcessInAsyncFunction,
Rule::WaitForProcessInAsyncFunction,
]) {
flake8_async::rules::blocking_process_invocation(checker, call);
}
if checker.enabled(Rule::BlockingSleepInAsyncFunction) {
flake8_async::rules::blocking_sleep(checker, call);
}
if checker.enabled(Rule::SleepForeverCall) {
flake8_async::rules::sleep_forever_call(checker, call);
@@ -963,10 +970,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::no_implicit_cwd(checker, call);
}
if checker.enabled(Rule::TrioSyncCall) {
flake8_trio::rules::sync_call(checker, call);
flake8_async::rules::sync_call(checker, call);
}
if checker.enabled(Rule::TrioZeroSleepCall) {
flake8_trio::rules::zero_sleep_call(checker, call);
flake8_async::rules::zero_sleep_call(checker, call);
}
if checker.enabled(Rule::UnnecessaryDunderCall) {
pylint::rules::unnecessary_dunder_call(checker, call);
@@ -1515,16 +1522,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::reimplemented_starmap(checker, &generator.into());
}
}
Expr::BoolOp(
bool_op @ ast::ExprBoolOp {
op,
values,
range: _,
},
) => {
if checker.enabled(Rule::RepeatedIsinstanceCalls) {
pylint::rules::repeated_isinstance_calls(checker, expr, *op, values);
}
Expr::BoolOp(bool_op) => {
if checker.enabled(Rule::MultipleStartsEndsWith) {
flake8_pie::rules::multiple_starts_ends_with(checker, expr);
}

View File

@@ -8,11 +8,11 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::{
airflow, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_debugger,
flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie, flake8_pyi,
flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_trio, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
airflow, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie,
flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
@@ -357,7 +357,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::TrioAsyncFunctionWithTimeout) {
flake8_trio::rules::async_function_with_timeout(checker, function_def);
flake8_async::rules::async_function_with_timeout(checker, function_def);
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
@@ -1303,7 +1303,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::useless_with_lock(checker, with_stmt);
}
if checker.enabled(Rule::TrioTimeoutWithoutAwait) {
flake8_trio::rules::timeout_without_await(checker, with_stmt, items);
flake8_async::rules::timeout_without_await(checker, with_stmt, items);
}
}
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
@@ -1320,7 +1320,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
perflint::rules::try_except_in_loop(checker, body);
}
if checker.enabled(Rule::TrioUnneededSleep) {
flake8_trio::rules::unneeded_sleep(checker, while_stmt);
flake8_async::rules::unneeded_sleep(checker, while_stmt);
}
}
Stmt::For(

View File

@@ -462,8 +462,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| imports::is_sys_path_modification(stmt, self.semantic())
|| (self.settings.preview.is_enabled()
&& imports::is_os_environ_modification(stmt, self.semantic())))
|| imports::is_os_environ_modification(stmt, self.semantic()))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}

View File

@@ -57,9 +57,6 @@ pub enum RuleGroup {
Deprecated,
/// The rule has been removed, errors will be displayed on use.
Removed,
/// Legacy category for unstable rules, supports backwards compatible selection.
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
Nursery,
}
#[ruff_macros::map_codes]
@@ -71,72 +68,39 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
Some(match (linter, code) {
// pycodestyle errors
(Pycodestyle, "E101") => (RuleGroup::Stable, rules::pycodestyle::rules::MixedSpacesAndTabs),
#[allow(deprecated)]
(Pycodestyle, "E111") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
#[allow(deprecated)]
(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
#[allow(deprecated)]
(Pycodestyle, "E113") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
#[allow(deprecated)]
(Pycodestyle, "E114") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
#[allow(deprecated)]
(Pycodestyle, "E115") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E116") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
#[allow(deprecated)]
(Pycodestyle, "E117") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::OverIndented),
#[allow(deprecated)]
(Pycodestyle, "E201") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
#[allow(deprecated)]
(Pycodestyle, "E202") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
#[allow(deprecated)]
(Pycodestyle, "E203") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
#[allow(deprecated)]
(Pycodestyle, "E211") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
#[allow(deprecated)]
(Pycodestyle, "E221") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E222") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E223") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E224") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E225") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
#[allow(deprecated)]
(Pycodestyle, "E226") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
#[allow(deprecated)]
(Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
#[allow(deprecated)]
(Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
#[allow(deprecated)]
(Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
#[allow(deprecated)]
(Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E262") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E265") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E266") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E271") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E272") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E273") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E111") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
(Pycodestyle, "E112") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
(Pycodestyle, "E113") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
(Pycodestyle, "E114") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
(Pycodestyle, "E115") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
(Pycodestyle, "E116") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
(Pycodestyle, "E117") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::OverIndented),
(Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
(Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
(Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
(Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
(Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
(Pycodestyle, "E223") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
(Pycodestyle, "E224") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
(Pycodestyle, "E225") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
(Pycodestyle, "E226") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
(Pycodestyle, "E227") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
(Pycodestyle, "E228") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
(Pycodestyle, "E231") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
(Pycodestyle, "E241") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
(Pycodestyle, "E242") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterComma),
(Pycodestyle, "E251") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
(Pycodestyle, "E252") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
(Pycodestyle, "E261") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
(Pycodestyle, "E262") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
(Pycodestyle, "E265") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
(Pycodestyle, "E266") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
(Pycodestyle, "E271") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
(Pycodestyle, "E272") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
(Pycodestyle, "E273") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
(Pycodestyle, "E274") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
(Pycodestyle, "E275") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods),
(Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel),
(Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines),
@@ -161,7 +125,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E742") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousClassName),
(Pycodestyle, "E743") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousFunctionName),
(Pycodestyle, "E902") => (RuleGroup::Stable, rules::pycodestyle::rules::IOError),
(Pycodestyle, "E999") => (RuleGroup::Stable, rules::pycodestyle::rules::SyntaxError),
(Pycodestyle, "E999") => (RuleGroup::Deprecated, rules::pycodestyle::rules::SyntaxError),
// pycodestyle warnings
(Pycodestyle, "W191") => (RuleGroup::Stable, rules::pycodestyle::rules::TabIndentation),
@@ -226,16 +190,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
#[allow(deprecated)]
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
(Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiImportName),
(Pylint, "C2701") => (RuleGroup::Preview, rules::pylint::rules::ImportPrivateName),
(Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall),
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "E0100") => (RuleGroup::Stable, rules::pylint::rules::YieldInInit),
(Pylint, "E0101") => (RuleGroup::Stable, rules::pylint::rules::ReturnInInit),
(Pylint, "E0115") => (RuleGroup::Preview, rules::pylint::rules::NonlocalAndGlobal),
(Pylint, "E0115") => (RuleGroup::Stable, rules::pylint::rules::NonlocalAndGlobal),
(Pylint, "E0116") => (RuleGroup::Stable, rules::pylint::rules::ContinueInFinally),
(Pylint, "E0117") => (RuleGroup::Stable, rules::pylint::rules::NonlocalWithoutBinding),
(Pylint, "E0118") => (RuleGroup::Stable, rules::pylint::rules::LoadBeforeGlobalDeclaration),
@@ -250,9 +213,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0309") => (RuleGroup::Preview, rules::pylint::rules::InvalidHashReturnType),
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E0643") => (RuleGroup::Stable, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Stable, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Stable, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1141") => (RuleGroup::Preview, rules::pylint::rules::DictIterMissingItems),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
(Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs),
@@ -285,64 +248,62 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1701") => (RuleGroup::Removed, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1704") => (RuleGroup::Stable, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1730") => (RuleGroup::Preview, rules::pylint::rules::IfStmtMinMax),
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R1736") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R2044") => (RuleGroup::Preview, rules::pylint::rules::EmptyComment),
(Pylint, "R2044") => (RuleGroup::Stable, rules::pylint::rules::EmptyComment),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6104") => (RuleGroup::Preview, rules::pylint::rules::NonAugmentedAssignment),
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
#[allow(deprecated)]
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "R6301") => (RuleGroup::Preview, rules::pylint::rules::NoSelfUse),
(Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda),
(Pylint, "W0177") => (RuleGroup::Preview, rules::pylint::rules::NanComparison),
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0128") => (RuleGroup::Preview, rules::pylint::rules::RedeclaredAssignedName),
(Pylint, "W0128") => (RuleGroup::Stable, rules::pylint::rules::RedeclaredAssignedName),
(Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral),
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
(Pylint, "W0133") => (RuleGroup::Preview, rules::pylint::rules::UselessExceptionStatement),
(Pylint, "W0133") => (RuleGroup::Stable, rules::pylint::rules::UselessExceptionStatement),
(Pylint, "W0211") => (RuleGroup::Preview, rules::pylint::rules::BadStaticmethodArgument),
(Pylint, "W0245") => (RuleGroup::Preview, rules::pylint::rules::SuperWithoutBrackets),
(Pylint, "W0245") => (RuleGroup::Stable, rules::pylint::rules::SuperWithoutBrackets),
(Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf),
(Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned),
(Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement),
(Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0604") => (RuleGroup::Stable, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0642") => (RuleGroup::Preview, rules::pylint::rules::SelfOrClsAssignment),
(Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException),
(Pylint, "W1501") => (RuleGroup::Preview, rules::pylint::rules::BadOpenMode),
(Pylint, "W1501") => (RuleGroup::Stable, rules::pylint::rules::BadOpenMode),
(Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Stable, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Stable, rules::pylint::rules::SubprocessRunWithoutCheck),
(Pylint, "W1514") => (RuleGroup::Preview, rules::pylint::rules::UnspecifiedEncoding),
#[allow(deprecated)]
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2101") => (RuleGroup::Preview, rules::pylint::rules::UselessWithLock),
(Pylint, "W1641") => (RuleGroup::Preview, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2101") => (RuleGroup::Stable, rules::pylint::rules::UselessWithLock),
(Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName),
#[allow(deprecated)]
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3201") => (RuleGroup::Preview, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3301") => (RuleGroup::Stable, rules::pylint::rules::NestedMinMax),
// flake8-async
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "101") => (RuleGroup::Stable, rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction),
(Flake8Async, "102") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOsCallInAsyncFunction),
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::TrioTimeoutWithoutAwait),
(Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall),
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::TrioAsyncFunctionWithTimeout),
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::TrioUnneededSleep),
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::TrioZeroSleepCall),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),
// flake8-trio
(Flake8Trio, "100") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
(Flake8Trio, "105") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioSyncCall),
(Flake8Trio, "109") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioAsyncFunctionWithTimeout),
(Flake8Trio, "110") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioUnneededSleep),
(Flake8Trio, "115") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioZeroSleepCall),
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
(Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction),
(Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction),
// flake8-builtins
(Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing),
@@ -515,8 +476,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "911") => (RuleGroup::Stable, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
#[allow(deprecated)]
(Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
(Flake8Copyright, "001") => (RuleGroup::Preview, rules::flake8_copyright::rules::MissingCopyrightNotice),
// pyupgrade
(Pyupgrade, "001") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessMetaclassType),
@@ -701,7 +661,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
(Flake8Bandit, "610") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoExtra),
(Flake8Bandit, "610") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoExtra),
(Flake8Bandit, "611") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoRawSql),
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
@@ -971,9 +931,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
@@ -992,9 +952,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
#[cfg(any(feature = "test-rules", test))]
(Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule),
#[cfg(any(feature = "test-rules", test))]
#[allow(deprecated)]
(Ruff, "912") => (RuleGroup::Nursery, rules::ruff::rules::NurseryTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "920") => (RuleGroup::Deprecated, rules::ruff::rules::DeprecatedTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "921") => (RuleGroup::Deprecated, rules::ruff::rules::AnotherDeprecatedTestRule),
@@ -1041,7 +998,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Perflint, "203") => (RuleGroup::Stable, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Stable, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Stable, rules::perflint::rules::ManualListCopy),
(Perflint, "403") => (RuleGroup::Preview, rules::perflint::rules::ManualDictComprehension),
(Perflint, "403") => (RuleGroup::Stable, rules::perflint::rules::ManualDictComprehension),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsFixme),
@@ -1057,18 +1014,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// refurb
(Refurb, "101") => (RuleGroup::Preview, rules::refurb::rules::ReadWholeFile),
(Refurb, "103") => (RuleGroup::Preview, rules::refurb::rules::WriteWholeFile),
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
(Refurb, "105") => (RuleGroup::Stable, rules::refurb::rules::PrintEmptyString),
(Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator),
#[allow(deprecated)]
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
(Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend),
(Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat),
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
(Refurb, "129") => (RuleGroup::Preview, rules::refurb::rules::ReadlinesInFor),
#[allow(deprecated)]
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Preview, rules::refurb::rules::IfExprMinMax),
(Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor),
(Refurb, "131") => (RuleGroup::Preview, rules::refurb::rules::DeleteFullSlice),
(Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
@@ -1076,18 +1030,18 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "154") => (RuleGroup::Preview, rules::refurb::rules::RepeatedGlobal),
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),
(Refurb, "167") => (RuleGroup::Preview, rules::refurb::rules::RegexFlagAlias),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "167") => (RuleGroup::Stable, rules::refurb::rules::RegexFlagAlias),
(Refurb, "168") => (RuleGroup::Stable, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Stable, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
(Refurb, "177") => (RuleGroup::Stable, rules::refurb::rules::ImplicitCwd),
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
(Refurb, "187") => (RuleGroup::Preview, rules::refurb::rules::ListReverseCopy),
(Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex),
(Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy),
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
// flake8-logging

View File

@@ -11,7 +11,7 @@ pub use registry::clap_completion::RuleParser;
#[cfg(feature = "clap")]
pub use rule_selector::clap_completion::RuleSelectorParser;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::{IOError, SyntaxError};
pub use rules::pycodestyle::rules::IOError;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@@ -5,7 +5,6 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use log::error;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -26,11 +25,9 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{fix_file, FixResult};
use crate::logging::DisplayParseError;
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule, RuleSet};
use crate::rules::pycodestyle;
#[cfg(any(feature = "test-rules", test))]
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
use crate::settings::types::UnsafeFixes;
@@ -38,29 +35,19 @@ use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
/// 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.
pub struct LinterResult<T> {
pub data: T,
pub error: Option<ParseError>,
}
impl<T> LinterResult<T> {
const fn new(data: T, error: Option<ParseError>) -> Self {
Self { data, error }
}
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> LinterResult<U> {
LinterResult::new(f(self.data), self.error)
}
pub struct LinterResult {
/// A collection of diagnostic messages generated by the linter.
pub messages: Vec<Message>,
/// A flag indicating the presence of syntax errors in the source file.
/// If `true`, at least one syntax error was detected in the source file.
pub has_syntax_error: bool,
}
pub type FixTable = FxHashMap<Rule, usize>;
pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult<Vec<Message>>,
pub result: LinterResult,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, SourceKind>,
/// The number of fixes applied for each [`Rule`].
@@ -82,10 +69,9 @@ pub fn check_path(
source_kind: &SourceKind,
source_type: PySourceType,
parsed: &Parsed<ModModule>,
) -> LinterResult<Vec<Diagnostic>> {
) -> Vec<Diagnostic> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
let mut error = None;
let tokens = parsed.tokens();
let comment_ranges = indexer.comment_ranges();
@@ -142,67 +128,53 @@ pub fn check_path(
));
}
// Run the AST-based rules.
let use_ast = settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_ast());
let use_imports = !directives.isort.skip_file
&& settings
// Run the AST-based rules only if there are no syntax errors.
if parsed.is_valid() {
let use_ast = settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match parsed.as_result() {
Ok(parsed) => {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
));
}
if use_imports {
let import_diagnostics = check_imports(
parsed,
locator,
indexer,
&directives.isort,
settings,
stylist,
package,
source_type,
cell_offsets,
);
diagnostics.extend(import_diagnostics);
}
if use_doc_lines {
doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator));
}
.any(|rule_code| rule_code.lint_source().is_ast());
let use_imports = !directives.isort.skip_file
&& settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
if use_ast {
diagnostics.extend(check_ast(
parsed,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
noqa,
path,
package,
source_type,
cell_offsets,
notebook_index,
));
}
Err(parse_errors) => {
// Always add a diagnostic for the syntax error, regardless of whether
// `Rule::SyntaxError` is enabled. We avoid propagating the syntax error
// if it's disabled via any of the usual mechanisms (e.g., `noqa`,
// `per-file-ignores`), and the easiest way to detect that suppression is
// to see if the diagnostic persists to the end of the function.
for parse_error in parse_errors {
pycodestyle::rules::syntax_error(&mut diagnostics, parse_error, locator);
}
// TODO(dhruvmanila): Remove this clone
error = parse_errors.iter().next().cloned();
if use_imports {
let import_diagnostics = check_imports(
parsed,
locator,
indexer,
&directives.isort,
settings,
stylist,
package,
source_type,
cell_offsets,
);
diagnostics.extend(import_diagnostics);
}
if use_doc_lines {
doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator));
}
}
}
@@ -244,9 +216,6 @@ pub fn check_path(
Rule::StableTestRuleDisplayOnlyFix => {
test_rules::StableTestRuleDisplayOnlyFix::diagnostic(locator, comment_ranges)
}
Rule::NurseryTestRule => {
test_rules::NurseryTestRule::diagnostic(locator, comment_ranges)
}
Rule::PreviewTestRule => {
test_rules::PreviewTestRule::diagnostic(locator, comment_ranges)
}
@@ -308,7 +277,7 @@ pub fn check_path(
locator,
comment_ranges,
&directives.noqa_line_for,
error.is_none(),
parsed.is_valid(),
&per_file_ignores,
settings,
);
@@ -319,23 +288,6 @@ pub fn check_path(
}
}
// If there was a syntax error, check if it should be discarded.
if error.is_some() {
// If the syntax error was removed by _any_ of the above disablement methods (e.g., a
// `noqa` directive, or a `per-file-ignore`), discard it.
if !diagnostics
.iter()
.any(|diagnostic| diagnostic.kind.rule() == Rule::SyntaxError)
{
error = None;
}
// If the syntax error _diagnostic_ is disabled, discard the _diagnostic_.
if !settings.rules.enabled(Rule::SyntaxError) {
diagnostics.retain(|diagnostic| diagnostic.kind.rule() != Rule::SyntaxError);
}
}
// Remove fixes for any rules marked as unfixable.
for diagnostic in &mut diagnostics {
if !settings.rules.should_fix(diagnostic.kind.rule()) {
@@ -355,7 +307,7 @@ pub fn check_path(
}
}
LinterResult::new(diagnostics, error)
diagnostics
}
const MAX_ITERATIONS: usize = 100;
@@ -389,10 +341,7 @@ pub fn add_noqa_to_path(
);
// Generate diagnostics, ignoring any existing `noqa` directives.
let LinterResult {
data: diagnostics,
error,
} = check_path(
let diagnostics = check_path(
path,
package,
&locator,
@@ -406,19 +355,6 @@ pub fn add_noqa_to_path(
&parsed,
);
// Log any parse errors.
if let Some(error) = error {
error!(
"{}",
DisplayParseError::from_source_code(
error,
Some(path.to_path_buf()),
&locator.to_source_code(),
source_kind,
)
);
}
// Add any missing `# noqa` pragmas.
// TODO(dhruvmanila): Add support for Jupyter Notebooks
add_noqa(
@@ -442,7 +378,7 @@ pub fn lint_only(
source_kind: &SourceKind,
source_type: PySourceType,
source: ParseSource,
) -> LinterResult<Vec<Message>> {
) -> LinterResult {
let parsed = source.into_parsed(source_kind, source_type);
// Map row and column locations to byte slices (lazily).
@@ -463,7 +399,7 @@ pub fn lint_only(
);
// Generate diagnostics.
let result = check_path(
let diagnostics = check_path(
path,
package,
&locator,
@@ -477,12 +413,22 @@ pub fn lint_only(
&parsed,
);
result.map(|diagnostics| diagnostics_to_messages(diagnostics, path, &locator, &directives))
LinterResult {
messages: diagnostics_to_messages(
diagnostics,
parsed.errors(),
path,
&locator,
&directives,
),
has_syntax_error: !parsed.is_valid(),
}
}
/// Convert from diagnostics to messages.
fn diagnostics_to_messages(
diagnostics: Vec<Diagnostic>,
parse_errors: &[ParseError],
path: &Path,
locator: &Locator,
directives: &Directives,
@@ -498,12 +444,13 @@ fn diagnostics_to_messages(
builder.finish()
});
diagnostics
.into_iter()
.map(|diagnostic| {
parse_errors
.iter()
.map(|parse_error| Message::from_parse_error(parse_error, locator, file.deref().clone()))
.chain(diagnostics.into_iter().map(|diagnostic| {
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
})
}))
.collect()
}
@@ -527,8 +474,8 @@ pub fn lint_fix<'a>(
// As an escape hatch, bail after 100 iterations.
let mut iterations = 0;
// Track whether the _initial_ source code was parseable.
let mut parseable = false;
// Track whether the _initial_ source code is valid syntax.
let mut is_valid_syntax = false;
// Continuously fix until the source code stabilizes.
loop {
@@ -554,7 +501,7 @@ pub fn lint_fix<'a>(
);
// Generate diagnostics.
let result = check_path(
let diagnostics = check_path(
path,
package,
&locator,
@@ -569,19 +516,21 @@ pub fn lint_fix<'a>(
);
if iterations == 0 {
parseable = result.error.is_none();
is_valid_syntax = parsed.is_valid();
} else {
// If the source code was parseable on the first pass, but is no
// longer parseable on a subsequent pass, then we've introduced a
// syntax error. Return the original code.
if parseable && result.error.is_some() {
report_fix_syntax_error(
path,
transformed.source_code(),
&result.error.unwrap(),
fixed.keys().copied(),
);
return Err(anyhow!("Fix introduced a syntax error"));
if is_valid_syntax {
if let Some(error) = parsed.errors().first() {
report_fix_syntax_error(
path,
transformed.source_code(),
error,
fixed.keys().copied(),
);
return Err(anyhow!("Fix introduced a syntax error"));
}
}
}
@@ -590,7 +539,7 @@ pub fn lint_fix<'a>(
code: fixed_contents,
fixes: applied,
source_map,
}) = fix_file(&result.data, &locator, unsafe_fixes)
}) = fix_file(&diagnostics, &locator, unsafe_fixes)
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
@@ -607,13 +556,20 @@ pub fn lint_fix<'a>(
continue;
}
report_failed_to_converge_error(path, transformed.source_code(), &result.data);
report_failed_to_converge_error(path, transformed.source_code(), &diagnostics);
}
return Ok(FixerResult {
result: result.map(|diagnostics| {
diagnostics_to_messages(diagnostics, path, &locator, &directives)
}),
result: LinterResult {
messages: diagnostics_to_messages(
diagnostics,
parsed.errors(),
path,
&locator,
&directives,
),
has_syntax_error: !is_valid_syntax,
},
transformed,
fixed,
});

View File

@@ -3,7 +3,6 @@ use std::io::Write;
use ruff_source_file::SourceLocation;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate error logging commands for Azure Pipelines format.
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
@@ -29,12 +28,14 @@ impl Emitter for AzureEmitter {
writeln!(
writer,
"##vso[task.logissue type=error\
;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}",
;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}",
filename = message.filename(),
line = location.row,
col = location.column,
code = message.kind.rule().noqa_code(),
body = message.kind.body,
code = message
.rule()
.map_or_else(String::new, |rule| format!("code={};", rule.noqa_code())),
body = message.body(),
)?;
}
@@ -46,7 +47,9 @@ impl Emitter for AzureEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::AzureEmitter;
#[test]
@@ -56,4 +59,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = AzureEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -27,8 +27,8 @@ pub(super) struct Diff<'a> {
impl<'a> Diff<'a> {
pub(crate) fn from_message(message: &'a Message) -> Option<Diff> {
message.fix.as_ref().map(|fix| Diff {
source_code: &message.file,
message.fix().map(|fix| Diff {
source_code: message.source_file(),
fix,
})
}

View File

@@ -4,7 +4,6 @@ use ruff_source_file::SourceLocation;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate error workflow command in GitHub Actions format.
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
@@ -32,9 +31,8 @@ impl Emitter for GithubEmitter {
write!(
writer,
"::error title=Ruff \
({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message.kind.rule().noqa_code(),
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message.rule().map_or_else(String::new, |rule| format!(" ({})", rule.noqa_code())),
file = message.filename(),
row = source_location.row,
column = source_location.column,
@@ -42,15 +40,19 @@ impl Emitter for GithubEmitter {
end_column = end_location.column,
)?;
writeln!(
write!(
writer,
"{path}:{row}:{column}: {code} {body}",
"{path}:{row}:{column}:",
path = relativize_path(message.filename()),
row = location.row,
column = location.column,
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;
if let Some(rule) = message.rule() {
write!(writer, " {}", rule.noqa_code())?;
}
writeln!(writer, " {}", message.body())?;
}
Ok(())
@@ -61,7 +63,9 @@ impl Emitter for GithubEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::GithubEmitter;
#[test]
@@ -71,4 +75,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = GithubEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -9,7 +9,6 @@ use serde_json::json;
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate JSON with violations in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
@@ -91,8 +90,14 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);
let description = if let Some(rule) = message.rule() {
format!("({}) {}", rule.noqa_code(), message.body())
} else {
message.body().to_string()
};
let value = json!({
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
"description": description,
"severity": "major",
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
@@ -110,18 +115,10 @@ impl Serialize for SerializedMessages<'_> {
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
let Message {
kind,
range: _,
fix: _fix,
file: _,
noqa_offset: _,
} = message;
let mut hasher = DefaultHasher::new();
salt.hash(&mut hasher);
kind.name.hash(&mut hasher);
message.name().hash(&mut hasher);
project_path.hash(&mut hasher);
hasher.finish()
@@ -131,7 +128,9 @@ fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::GitlabEmitter;
#[test]
@@ -142,6 +141,14 @@ mod tests {
assert_snapshot!(redact_fingerprint(&content));
}
#[test]
fn syntax_errors() {
let mut emitter = GitlabEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(redact_fingerprint(&content));
}
// Redact the fingerprint because the default hasher isn't stable across platforms.
fn redact_fingerprint(content: &str) -> String {
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;

View File

@@ -205,7 +205,9 @@ impl std::fmt::Write for PadAdapter<'_> {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::GroupedEmitter;
use crate::settings::types::UnsafeFixes;
@@ -217,6 +219,14 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = GroupedEmitter::default();
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn show_source() {
let mut emitter = GroupedEmitter::default().with_show_source(true);

View File

@@ -10,7 +10,6 @@ use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
#[derive(Default)]
pub struct JsonEmitter;
@@ -50,20 +49,22 @@ impl Serialize for ExpandedMessages<'_> {
}
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
let source_code = message.file.to_source_code();
let source_code = message.source_file().to_source_code();
let notebook_index = context.notebook_index(message.filename());
let fix = message.fix.as_ref().map(|fix| {
let fix = message.fix().map(|fix| {
json!({
"applicability": fix.applicability(),
"message": message.kind.suggestion.as_deref(),
"message": message.suggestion(),
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index },
})
});
let mut start_location = source_code.source_location(message.start());
let mut end_location = source_code.source_location(message.end());
let mut noqa_location = source_code.source_location(message.noqa_offset);
let mut noqa_location = message
.noqa_offset()
.map(|offset| source_code.source_location(offset));
let mut notebook_cell_index = None;
if let Some(notebook_index) = notebook_index {
@@ -74,19 +75,19 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext)
);
start_location = notebook_index.translate_location(&start_location);
end_location = notebook_index.translate_location(&end_location);
noqa_location = notebook_index.translate_location(&noqa_location);
noqa_location = noqa_location.map(|location| notebook_index.translate_location(&location));
}
json!({
"code": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
"message": message.kind.body,
"code": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
"message": message.body(),
"fix": fix,
"cell": notebook_cell_index,
"location": start_location,
"end_location": end_location,
"filename": message.filename(),
"noqa_row": noqa_location.row
"noqa_row": noqa_location.map(|location| location.row)
})
}
@@ -170,7 +171,7 @@ mod tests {
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
create_notebook_messages, create_syntax_error_messages,
};
use crate::message::JsonEmitter;
@@ -182,6 +183,14 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JsonEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonEmitter;

View File

@@ -29,7 +29,7 @@ mod tests {
use crate::message::json_lines::JsonLinesEmitter;
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
create_notebook_messages, create_syntax_error_messages,
};
#[test]
@@ -40,6 +40,14 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JsonLinesEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = JsonLinesEmitter;

View File

@@ -8,7 +8,6 @@ use ruff_source_file::SourceLocation;
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::registry::AsRule;
#[derive(Default)]
pub struct JunitEmitter;
@@ -44,7 +43,7 @@ impl Emitter for JunitEmitter {
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
status.set_message(message.body());
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
@@ -57,10 +56,14 @@ impl Emitter for JunitEmitter {
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
body = message.body()
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
if let Some(rule) = message.rule() {
format!("org.ruff.{}", rule.noqa_code())
} else {
"org.ruff".to_string()
},
status,
);
let file_path = Path::new(filename);
@@ -88,7 +91,9 @@ impl Emitter for JunitEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::JunitEmitter;
#[test]
@@ -98,4 +103,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -14,13 +14,18 @@ pub use json_lines::JsonLinesEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
pub use rdjson::RdjsonEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
pub use text::TextEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::NotebookIndex;
use ruff_python_parser::ParseError;
use ruff_source_file::{Locator, SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::logging::DisplayParseErrorType;
use crate::registry::{AsRule, Rule};
mod azure;
mod diff;
mod github;
@@ -34,8 +39,17 @@ mod rdjson;
mod sarif;
mod text;
/// Message represents either a diagnostic message corresponding to a rule violation or a syntax
/// error message raised by the parser.
#[derive(Debug, PartialEq, Eq)]
pub struct Message {
pub enum Message {
Diagnostic(DiagnosticMessage),
SyntaxError(SyntaxErrorMessage),
}
/// A diagnostic message corresponding to a rule violation.
#[derive(Debug, PartialEq, Eq)]
pub struct DiagnosticMessage {
pub kind: DiagnosticKind,
pub range: TextRange,
pub fix: Option<Fix>,
@@ -43,37 +57,174 @@ pub struct Message {
pub noqa_offset: TextSize,
}
/// A syntax error message raised by the parser.
#[derive(Debug, PartialEq, Eq)]
pub struct SyntaxErrorMessage {
pub message: String,
pub range: TextRange,
pub file: SourceFile,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageKind {
Diagnostic(Rule),
SyntaxError,
}
impl MessageKind {
pub fn as_str(&self) -> &str {
match self {
MessageKind::Diagnostic(rule) => rule.as_ref(),
MessageKind::SyntaxError => "syntax-error",
}
}
}
impl Message {
/// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation.
pub fn from_diagnostic(
diagnostic: Diagnostic,
file: SourceFile,
noqa_offset: TextSize,
) -> Self {
Self {
) -> Message {
Message::Diagnostic(DiagnosticMessage {
range: diagnostic.range(),
kind: diagnostic.kind,
fix: diagnostic.fix,
file,
noqa_offset,
})
}
/// Create a [`Message`] from the given [`ParseError`].
pub fn from_parse_error(
parse_error: &ParseError,
locator: &Locator,
file: SourceFile,
) -> Message {
// Try to create a non-empty range so that the diagnostic can print a caret at the right
// position. This requires that we retrieve the next character, if any, and take its length
// to maintain char-boundaries.
let len = locator
.after(parse_error.location.start())
.chars()
.next()
.map_or(TextSize::new(0), TextLen::text_len);
Message::SyntaxError(SyntaxErrorMessage {
message: format!(
"SyntaxError: {}",
DisplayParseErrorType::new(&parse_error.error)
),
range: TextRange::at(parse_error.location.start(), len),
file,
})
}
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
match self {
Message::Diagnostic(m) => Some(m),
Message::SyntaxError(_) => None,
}
}
/// Returns `true` if `self` is a syntax error message.
pub const fn is_syntax_error(&self) -> bool {
matches!(self, Message::SyntaxError(_))
}
/// Returns a message kind.
pub fn kind(&self) -> MessageKind {
match self {
Message::Diagnostic(m) => MessageKind::Diagnostic(m.kind.rule()),
Message::SyntaxError(_) => MessageKind::SyntaxError,
}
}
/// Returns the name used to represent the diagnostic.
pub fn name(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.name,
Message::SyntaxError(_) => "SyntaxError",
}
}
/// Returns the message body to display to the user.
pub fn body(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.body,
Message::SyntaxError(m) => &m.message,
}
}
/// Returns the fix suggestion for the violation.
pub fn suggestion(&self) -> Option<&str> {
match self {
Message::Diagnostic(m) => m.kind.suggestion.as_deref(),
Message::SyntaxError(_) => None,
}
}
/// Returns the offset at which the `noqa` comment will be placed if it's a diagnostic message.
pub fn noqa_offset(&self) -> Option<TextSize> {
match self {
Message::Diagnostic(m) => Some(m.noqa_offset),
Message::SyntaxError(_) => None,
}
}
/// Returns the [`Fix`] for the message, if there is any.
pub fn fix(&self) -> Option<&Fix> {
match self {
Message::Diagnostic(m) => m.fix.as_ref(),
Message::SyntaxError(_) => None,
}
}
/// Returns `true` if the message contains a [`Fix`].
pub fn fixable(&self) -> bool {
self.fix().is_some()
}
/// Returns the [`Rule`] corresponding to the diagnostic message.
pub fn rule(&self) -> Option<Rule> {
match self {
Message::Diagnostic(m) => Some(m.kind.rule()),
Message::SyntaxError(_) => None,
}
}
/// Returns the filename for the message.
pub fn filename(&self) -> &str {
self.file.name()
self.source_file().name()
}
/// Computes the start source location for the message.
pub fn compute_start_location(&self) -> SourceLocation {
self.file.to_source_code().source_location(self.start())
self.source_file()
.to_source_code()
.source_location(self.start())
}
/// Computes the end source location for the message.
pub fn compute_end_location(&self) -> SourceLocation {
self.file.to_source_code().source_location(self.end())
self.source_file()
.to_source_code()
.source_location(self.end())
}
/// Returns the [`SourceFile`] which the message belongs to.
pub fn source_file(&self) -> &SourceFile {
match self {
Message::Diagnostic(m) => &m.file,
Message::SyntaxError(m) => &m.file,
}
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(&self.file, self.start()).cmp(&(&other.file, other.start()))
(self.source_file(), self.start()).cmp(&(other.source_file(), other.start()))
}
}
@@ -85,7 +236,10 @@ impl PartialOrd for Message {
impl Ranged for Message {
fn range(&self) -> TextRange {
self.range
match self {
Message::Diagnostic(m) => m.range,
Message::SyntaxError(m) => m.range,
}
}
}
@@ -155,11 +309,30 @@ mod tests {
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{OneIndexed, SourceFileBuilder};
use ruff_python_parser::{parse_unchecked, Mode};
use ruff_source_file::{Locator, OneIndexed, SourceFileBuilder};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};
pub(super) fn create_syntax_error_messages() -> Vec<Message> {
let source = r"from os import
if call(foo
def bar():
pass
";
let locator = Locator::new(source);
let source_file = SourceFileBuilder::new("syntax_errors.py", source).finish();
parse_unchecked(source, Mode::Module)
.errors()
.iter()
.map(|parse_error| {
Message::from_parse_error(parse_error, &locator, source_file.clone())
})
.collect()
}
pub(super) fn create_messages() -> Vec<Message> {
let fib = r#"import os

View File

@@ -4,7 +4,6 @@ use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate violations in Pylint format.
/// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter)
@@ -27,12 +26,20 @@ impl Emitter for PylintEmitter {
message.compute_start_location().row
};
let body = if let Some(rule) = message.rule() {
format!(
"[{code}] {body}",
code = rule.noqa_code(),
body = message.body()
)
} else {
message.body().to_string()
};
writeln!(
writer,
"{path}:{row}: [{code}] {body}",
"{path}:{row}: {body}",
path = relativize_path(message.filename()),
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;
}
@@ -44,7 +51,9 @@ impl Emitter for PylintEmitter {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::PylintEmitter;
#[test]
@@ -54,4 +63,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = PylintEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -9,7 +9,6 @@ use ruff_source_file::SourceCode;
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message, SourceLocation};
use crate::registry::AsRule;
#[derive(Default)]
pub struct RdjsonEmitter;
@@ -58,34 +57,34 @@ impl Serialize for ExpandedMessages<'_> {
}
fn message_to_rdjson_value(message: &Message) -> Value {
let source_code = message.file.to_source_code();
let source_code = message.source_file().to_source_code();
let start_location = source_code.source_location(message.start());
let end_location = source_code.source_location(message.end());
if let Some(fix) = message.fix.as_ref() {
if let Some(fix) = message.fix() {
json!({
"message": message.kind.body,
"message": message.body(),
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
"value": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
},
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
})
} else {
json!({
"message": message.kind.body,
"message": message.body(),
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
"value": message.rule().map(|rule| rule.noqa_code().to_string()),
"url": message.rule().and_then(|rule| rule.url()),
},
})
}
@@ -125,7 +124,9 @@ fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value {
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::RdjsonEmitter;
#[test]
@@ -135,4 +136,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = RdjsonEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -3,17 +3,16 @@ use std::io::Write;
use anyhow::Result;
use serde::{Serialize, Serializer};
use serde_json::json;
use strum::IntoEnumIterator;
use ruff_source_file::OneIndexed;
use crate::codes::Rule;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::{AsRule, Linter, RuleNamespace};
use crate::registry::{Linter, RuleNamespace};
use crate::VERSION;
use strum::IntoEnumIterator;
pub struct SarifEmitter;
impl Emitter for SarifEmitter {
@@ -103,7 +102,7 @@ impl Serialize for SarifRule<'_> {
#[derive(Debug)]
struct SarifResult {
rule: Rule,
rule: Option<Rule>,
level: String,
message: String,
uri: String,
@@ -120,9 +119,9 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.kind.rule(),
rule: message.rule(),
level: "error".to_string(),
message: message.kind.name.clone(),
message: message.name().to_string(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
@@ -140,9 +139,9 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.kind.rule(),
rule: message.rule(),
level: "error".to_string(),
message: message.kind.name.clone(),
message: message.name().to_string(),
uri: path.display().to_string(),
start_line: start_location.row,
start_column: start_location.column,
@@ -175,7 +174,7 @@ impl Serialize for SarifResult {
}
}
}],
"ruleId": self.rule.noqa_code().to_string(),
"ruleId": self.rule.map(|rule| rule.noqa_code().to_string()),
})
.serialize(serializer)
}
@@ -184,7 +183,9 @@ impl Serialize for SarifResult {
#[cfg(test)]
mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::tests::{
capture_emitter_output, create_messages, create_syntax_error_messages,
};
use crate::message::SarifEmitter;
fn get_output() -> String {
@@ -198,6 +199,13 @@ mod tests {
serde_json::from_str::<serde_json::Value>(&content).unwrap();
}
#[test]
fn valid_syntax_error_json() {
let mut emitter = SarifEmitter {};
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
serde_json::from_str::<serde_json::Value>(&content).unwrap();
}
#[test]
fn test_results() {
let content = get_output();

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff_linter/src/message/azure.rs
expression: content
---
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;]SyntaxError: Expected one or more symbol names after import
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;]SyntaxError: Expected ')', found newline

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff_linter/src/message/github.rs
expression: content
---
::error title=Ruff,file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
::error title=Ruff,file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: SyntaxError: Expected ')', found newline

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_linter/src/message/gitlab.rs
expression: redact_fingerprint(&content)
---
[
{
"description": "SyntaxError: Expected one or more symbol names after import",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 2
},
"path": "syntax_errors.py"
},
"severity": "major"
},
{
"description": "SyntaxError: Expected ')', found newline",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 3,
"end": 4
},
"path": "syntax_errors.py"
},
"severity": "major"
}
]

View File

@@ -0,0 +1,7 @@
---
source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
syntax_errors.py:
1:15 SyntaxError: Expected one or more symbol names after import
3:12 SyntaxError: Expected ')', found newline

View File

@@ -0,0 +1,40 @@
---
source: crates/ruff_linter/src/message/json.rs
expression: content
---
[
{
"cell": null,
"code": null,
"end_location": {
"column": 1,
"row": 2
},
"filename": "syntax_errors.py",
"fix": null,
"location": {
"column": 15,
"row": 1
},
"message": "SyntaxError: Expected one or more symbol names after import",
"noqa_row": null,
"url": null
},
{
"cell": null,
"code": null,
"end_location": {
"column": 1,
"row": 4
},
"filename": "syntax_errors.py",
"fix": null,
"location": {
"column": 12,
"row": 3
},
"message": "SyntaxError: Expected ')', found newline",
"noqa_row": null,
"url": null
}
]

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff_linter/src/message/json_lines.rs
expression: content
---
{"cell":null,"code":null,"end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"SyntaxError: Expected one or more symbol names after import","noqa_row":null,"url":null}
{"cell":null,"code":null,"end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"SyntaxError: Expected ')', found newline","noqa_row":null,"url":null}

View File

@@ -0,0 +1,15 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="2" failures="2" errors="0">
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
</testcase>
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
<failure message="SyntaxError: Expected &apos;)&apos;, found newline">line 3, col 12, SyntaxError: Expected &apos;)&apos;, found newline</failure>
</testcase>
</testsuite>
</testsuites>

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff_linter/src/message/pylint.rs
expression: content
---
syntax_errors.py:1: SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3: SyntaxError: Expected ')', found newline

View File

@@ -0,0 +1,53 @@
---
source: crates/ruff_linter/src/message/rdjson.rs
expression: content
---
{
"diagnostics": [
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "syntax_errors.py",
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 15,
"line": 1
}
}
},
"message": "SyntaxError: Expected one or more symbol names after import"
},
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "syntax_errors.py",
"range": {
"end": {
"column": 1,
"line": 4
},
"start": {
"column": 12,
"line": 3
}
}
},
"message": "SyntaxError: Expected ')', found newline"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
1 | from os import
| ^
2 |
3 | if call(foo
4 | def bar():
|
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|

View File

@@ -15,7 +15,6 @@ use crate::fs::relativize_path;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::settings::types::UnsafeFixes;
use crate::text_helpers::ShowNonprinting;
@@ -146,28 +145,33 @@ pub(super) struct RuleCodeAndBody<'a> {
impl Display for RuleCodeAndBody<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let kind = &self.message.kind;
if self.show_fix_status {
if let Some(fix) = self.message.fix.as_ref() {
if let Some(fix) = self.message.fix() {
// Do not display an indicator for unapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) {
if let Some(rule) = self.message.rule() {
write!(f, "{} ", rule.noqa_code().to_string().red().bold())?;
}
return write!(
f,
"{code} {fix}{body}",
code = kind.rule().noqa_code().to_string().red().bold(),
"{fix}{body}",
fix = format_args!("[{}] ", "*".cyan()),
body = kind.body,
body = self.message.body(),
);
}
}
};
write!(
f,
"{code} {body}",
code = kind.rule().noqa_code().to_string().red().bold(),
body = kind.body,
)
if let Some(rule) = self.message.rule() {
write!(
f,
"{code} {body}",
code = rule.noqa_code().to_string().red().bold(),
body = self.message.body(),
)
} else {
f.write_str(self.message.body())
}
}
}
@@ -178,11 +182,7 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let Message {
kind, file, range, ..
} = self.message;
let suggestion = kind.suggestion.as_deref();
let suggestion = self.message.suggestion();
let footer = if suggestion.is_some() {
vec![Annotation {
id: None,
@@ -193,9 +193,9 @@ impl Display for MessageCodeFrame<'_> {
Vec::new()
};
let source_code = file.to_source_code();
let source_code = self.message.source_file().to_source_code();
let content_start_index = source_code.line_index(range.start());
let content_start_index = source_code.line_index(self.message.start());
let mut start_index = content_start_index.saturating_sub(2);
// If we're working with a Jupyter Notebook, skip the lines which are
@@ -218,7 +218,7 @@ impl Display for MessageCodeFrame<'_> {
start_index = start_index.saturating_add(1);
}
let content_end_index = source_code.line_index(range.end());
let content_end_index = source_code.line_index(self.message.end());
let mut end_index = content_end_index
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
@@ -249,7 +249,7 @@ impl Display for MessageCodeFrame<'_> {
let source = replace_whitespace(
source_code.slice(TextRange::new(start_offset, end_offset)),
range - start_offset,
self.message.range() - start_offset,
);
let source_text = source.text.show_nonprinting();
@@ -260,7 +260,10 @@ impl Display for MessageCodeFrame<'_> {
let char_length = source.text[source.annotation_range].chars().count();
let label = kind.rule().noqa_code().to_string();
let label = self
.message
.rule()
.map_or_else(String::new, |rule| rule.noqa_code().to_string());
let snippet = Snippet {
title: None,
@@ -356,7 +359,7 @@ mod tests {
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_messages,
create_notebook_messages,
create_notebook_messages, create_syntax_error_messages,
};
use crate::message::TextEmitter;
use crate::settings::types::UnsafeFixes;
@@ -401,4 +404,12 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = TextEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
assert_snapshot!(content);
}
}

View File

@@ -1063,7 +1063,7 @@ mod tests {
use crate::generate_noqa_edits;
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
@@ -1380,4 +1380,36 @@ print(
))]
);
}
#[test]
fn syntax_error() {
let path = Path::new("/tmp/foo.txt");
let source = "\
foo;
bar =
";
let diagnostics = [Diagnostic::new(
UselessSemicolon,
TextRange::new(4.into(), 5.into()),
)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(
path,
&diagnostics,
&Locator::new(source),
&comment_ranges,
&[],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(
edits,
vec![Some(Edit::replacement(
" # noqa: E703\n".to_string(),
4.into(),
5.into()
))]
);
}
}

View File

@@ -64,9 +64,6 @@ pub enum Linter {
/// [flake8-async](https://pypi.org/project/flake8-async/)
#[prefix = "ASYNC"]
Flake8Async,
/// [flake8-trio](https://pypi.org/project/flake8-trio/)
#[prefix = "TRIO"]
Flake8Trio,
/// [flake8-bandit](https://pypi.org/project/flake8-bandit/)
#[prefix = "S"]
Flake8Bandit,

View File

@@ -103,6 +103,18 @@ static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
("TRY200", "B904"),
("PGH001", "S307"),
("PGH002", "G010"),
// flake8-trio and flake8-async merged with name flake8-async
("TRIO", "ASYNC1"),
("TRIO1", "ASYNC1"),
("TRIO10", "ASYNC10"),
("TRIO100", "ASYNC100"),
("TRIO105", "ASYNC105"),
("TRIO109", "ASYNC109"),
("TRIO11", "ASYNC11"),
("TRIO110", "ASYNC110"),
("TRIO115", "ASYNC115"),
// Removed in v0.5
("PLR1701", "SIM101"),
// Test redirect by exact code
#[cfg(any(feature = "test-rules", test))]
("RUF940", "RUF950"),

View File

@@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::codes::RuleCodePrefix;
use crate::codes::RuleIter;
use crate::codes::{RuleCodePrefix, RuleGroup};
use crate::registry::{Linter, Rule, RuleNamespace};
use crate::rule_redirects::get_redirect;
use crate::settings::types::PreviewMode;
@@ -15,9 +15,6 @@ use crate::settings::types::PreviewMode;
pub enum RuleSelector {
/// Select all rules (includes rules in preview if enabled)
All,
/// Legacy category to select all rules in the "nursery" which predated preview mode
#[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.
C,
@@ -65,8 +62,6 @@ impl FromStr for RuleSelector {
// **Changes should be reflected in `parse_no_redirect` as well**
match s {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
@@ -130,8 +125,6 @@ impl RuleSelector {
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
match self {
RuleSelector::All => ("", "ALL"),
#[allow(deprecated)]
RuleSelector::Nursery => ("", "NURSERY"),
RuleSelector::C => ("", "C"),
RuleSelector::T => ("", "T"),
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
@@ -191,10 +184,6 @@ impl RuleSelector {
match self {
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
#[allow(deprecated)]
RuleSelector::Nursery => {
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
}
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
@@ -216,19 +205,23 @@ impl RuleSelector {
pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator<Item = Rule> + 'a {
let preview_enabled = preview.mode.is_enabled();
let preview_require_explicit = preview.require_explicit;
#[allow(deprecated)]
self.all_rules().filter(move |rule| {
// Always include stable rules
rule.is_stable()
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|| ((self.is_exact() || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
// Enabling preview includes all preview or nursery rules unless explicit selection
// is turned on
|| ((rule.is_preview() || rule.is_nursery()) && preview_enabled && (self.is_exact() || !preview_require_explicit))
// Deprecated rules are excluded in preview mode unless explicitly selected
|| (rule.is_deprecated() && (!preview_enabled || self.is_exact()))
// Removed rules are included if explicitly selected but will error downstream
|| (rule.is_removed() && self.is_exact())
match rule.group() {
// Always include stable rules
RuleGroup::Stable => true,
// Enabling preview includes all preview rules unless explicit selection is turned on
RuleGroup::Preview => {
preview_enabled && (self.is_exact() || !preview_require_explicit)
}
// Deprecated rules are excluded in preview mode and with 'All' option unless explicitly selected
RuleGroup::Deprecated => {
(!preview_enabled || self.is_exact())
&& !matches!(self, RuleSelector::All { .. })
}
// Removed rules are included if explicitly selected but will error downstream
RuleGroup::Removed => self.is_exact(),
}
})
}
@@ -240,7 +233,6 @@ impl RuleSelector {
pub enum RuleSelectorIter {
All(RuleIter),
Nursery(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
Vec(std::vec::IntoIter<Rule>),
}
@@ -251,7 +243,6 @@ impl Iterator for RuleSelectorIter {
fn next(&mut self) -> Option<Self::Item> {
match self {
RuleSelectorIter::All(iter) => iter.next(),
RuleSelectorIter::Nursery(iter) => iter.next(),
RuleSelectorIter::Chain(iter) => iter.next(),
RuleSelectorIter::Vec(iter) => iter.next(),
}
@@ -288,7 +279,7 @@ mod schema {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
[
// Include the non-standard "ALL" and "NURSERY" selectors.
// Include the non-standard "ALL" selectors.
"ALL".to_string(),
// Include the legacy "C" and "T" selectors.
"C".to_string(),
@@ -345,8 +336,6 @@ impl RuleSelector {
pub fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
#[allow(deprecated)]
RuleSelector::Nursery => Specificity::All,
RuleSelector::T => Specificity::LinterGroup,
RuleSelector::C => Specificity::LinterGroup,
RuleSelector::Linter(..) => Specificity::Linter,
@@ -369,8 +358,6 @@ impl RuleSelector {
// **Changes should be reflected in `from_str` as well**
match s {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -13,10 +14,18 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC100.py"))]
#[test_case(Rule::OpenSleepOrSubprocessInAsyncFunction, Path::new("ASYNC101.py"))]
#[test_case(Rule::BlockingOsCallInAsyncFunction, Path::new("ASYNC102.py"))]
#[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("ASYNC100.py"))]
#[test_case(Rule::TrioSyncCall, Path::new("ASYNC105.py"))]
#[test_case(Rule::TrioAsyncFunctionWithTimeout, Path::new("ASYNC109.py"))]
#[test_case(Rule::TrioUnneededSleep, Path::new("ASYNC110.py"))]
#[test_case(Rule::TrioZeroSleepCall, Path::new("ASYNC115.py"))]
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))]
#[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.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

@@ -40,7 +40,7 @@ impl Violation for TrioAsyncFunctionWithTimeout {
}
}
/// TRIO109
/// ASYNC109
pub(crate) fn async_function_with_timeout(
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,

View File

@@ -45,6 +45,7 @@ fn is_blocking_http_call(qualified_name: &QualifiedName) -> bool {
matches!(
qualified_name.segments(),
["urllib", "request", "urlopen"]
| ["urllib3", "request"]
| [
"httpx" | "requests",
"get"
@@ -60,7 +61,7 @@ fn is_blocking_http_call(qualified_name: &QualifiedName) -> bool {
)
}
/// ASYNC100
/// ASYNC210
pub(crate) fn blocking_http_call(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().in_async_context() {
if checker

View File

@@ -7,8 +7,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
/// or `subprocess` methods.
/// Checks that async functions do not open files with blocking methods like `open`.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
@@ -21,61 +20,53 @@ use crate::checkers::ast::Checker;
/// ## Example
/// ```python
/// async def foo():
/// time.sleep(1000)
/// with open("bar.txt") as f:
/// contents = f.read()
/// ```
///
/// Use instead:
/// ```python
/// import anyio
///
///
/// async def foo():
/// await asyncio.sleep(1000)
/// async with await anyio.open_file("bar.txt") as f:
/// contents = await f.read()
/// ```
#[violation]
pub struct OpenSleepOrSubprocessInAsyncFunction;
pub struct BlockingOpenCallInAsyncFunction;
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
impl Violation for BlockingOpenCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
format!("Async functions should not open files with blocking methods like `open`")
}
}
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, call: &ast::ExprCall) {
/// ASYNC230
pub(crate) fn blocking_open_call(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().in_async_context() {
return;
}
if is_open_sleep_or_subprocess_call(&call.func, checker.semantic())
if is_open_call(&call.func, checker.semantic())
|| is_open_call_from_pathlib(call.func.as_ref(), checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction,
BlockingOpenCallInAsyncFunction,
call.func.range(),
));
}
}
/// Returns `true` if the expression resolves to a blocking call, like `time.sleep` or
/// `subprocess.run`.
fn is_open_sleep_or_subprocess_call(func: &Expr, semantic: &SemanticModel) -> bool {
/// Returns `true` if the expression resolves to a blocking open call, like `open` or `Path().open()`.
fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["" | "builtins", "open"]
| ["time", "sleep"]
| [
"subprocess",
"run"
| "Popen"
| "call"
| "check_call"
| "check_output"
| "getoutput"
| "getstatusoutput"
]
| ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"]
["" | "io", "open"] | ["io", "open_code"]
)
})
}

View File

@@ -1,82 +0,0 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::name::QualifiedName;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not contain calls to blocking synchronous
/// process calls via the `os` module.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.popen()
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// os.popen()
/// ```
#[violation]
pub struct BlockingOsCallInAsyncFunction;
impl Violation for BlockingOsCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call synchronous `os` methods")
}
}
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().seen_module(Modules::OS) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.as_ref()
.is_some_and(is_unsafe_os_method)
{
checker.diagnostics.push(Diagnostic::new(
BlockingOsCallInAsyncFunction,
call.func.range(),
));
}
}
}
}
fn is_unsafe_os_method(qualified_name: &QualifiedName) -> bool {
matches!(
qualified_name.segments(),
[
"os",
"popen"
| "posix_spawn"
| "posix_spawnp"
| "spawnl"
| "spawnle"
| "spawnlp"
| "spawnlpe"
| "spawnv"
| "spawnve"
| "spawnvp"
| "spawnvpe"
| "system"
]
)
}

View File

@@ -0,0 +1,166 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::find_assigned_value;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks that async functions do not create subprocesses with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.popen(cmd)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct CreateSubprocessInAsyncFunction;
impl Violation for CreateSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not create subprocesses with blocking methods")
}
}
/// ## What it does
/// Checks that async functions do not run processes with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// subprocess.run(cmd)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct RunProcessInAsyncFunction;
impl Violation for RunProcessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not run processes with blocking methods")
}
}
/// ## What it does
/// Checks that async functions do not wait on processes with blocking methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.waitpid(0)
/// ```
///
/// Use instead:
/// ```python
/// def wait_for_process():
/// os.waitpid(0)
///
///
/// async def foo():
/// await asyncio.loop.run_in_executor(None, wait_for_process)
/// ```
#[violation]
pub struct WaitForProcessInAsyncFunction;
impl Violation for WaitForProcessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not wait on processes with blocking methods")
}
}
/// ASYNC220, ASYNC221, ASYNC222
pub(crate) fn blocking_process_invocation(checker: &mut Checker, call: &ast::ExprCall) {
if !checker.semantic().in_async_context() {
return;
}
let Some(diagnostic_kind) =
checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.and_then(|qualified_name| match qualified_name.segments() {
["subprocess", "Popen"] | ["os", "popen"] => {
Some(CreateSubprocessInAsyncFunction.into())
}
["os", "system" | "posix_spawn" | "posix_spawnp"]
| ["subprocess", "run" | "call" | "check_call" | "check_output" | "getoutput"
| "getstatusoutput"] => Some(RunProcessInAsyncFunction.into()),
["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] => {
Some(WaitForProcessInAsyncFunction.into())
}
["os", "spawnl" | "spawnle" | "spawnlp" | "spawnlpe" | "spawnv" | "spawnve"
| "spawnvp" | "spawnvpe"] => {
if is_p_wait(call, checker.semantic()) {
Some(RunProcessInAsyncFunction.into())
} else {
Some(CreateSubprocessInAsyncFunction.into())
}
}
_ => None,
})
else {
return;
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, call.func.range());
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}
fn is_p_wait(call: &ast::ExprCall, semantic: &SemanticModel) -> bool {
let Some(arg) = call.arguments.find_argument("mode", 0) else {
return true;
};
if let Some(qualified_name) = semantic.resolve_qualified_name(arg) {
return matches!(qualified_name.segments(), ["os", "P_WAIT"]);
} else if let Expr::Name(ast::ExprName { id, .. }) = arg {
let Some(value) = find_assigned_value(id, semantic) else {
return false;
};
if let Some(qualified_name) = semantic.resolve_qualified_name(value) {
return matches!(qualified_name.segments(), ["os", "P_WAIT"]);
}
}
false
}

View File

@@ -0,0 +1,60 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::name::QualifiedName;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not call `time.sleep`.
///
/// ## Why is this bad?
/// Blocking an async function via a `time.sleep` call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// `time.sleep`, negating the benefits of asynchronous programming.
///
/// Instead of `time.sleep`, use `asyncio.sleep`.
///
/// ## Example
/// ```python
/// async def fetch():
/// time.sleep(1)
/// ```
///
/// Use instead:
/// ```python
/// async def fetch():
/// await asyncio.sleep(1)
/// ```
#[violation]
pub struct BlockingSleepInAsyncFunction;
impl Violation for BlockingSleepInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `time.sleep`")
}
}
fn is_blocking_sleep(qualified_name: &QualifiedName) -> bool {
matches!(qualified_name.segments(), ["time", "sleep"])
}
/// ASYNC251
pub(crate) fn blocking_sleep(checker: &mut Checker, call: &ExprCall) {
if checker.semantic().in_async_context() {
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.as_ref()
.is_some_and(is_blocking_sleep)
{
checker.diagnostics.push(Diagnostic::new(
BlockingSleepInAsyncFunction,
call.func.range(),
));
}
}
}

View File

@@ -1,9 +1,21 @@
pub(crate) use async_function_with_timeout::*;
pub(crate) use blocking_http_call::*;
pub(crate) use blocking_os_call::*;
pub(crate) use open_sleep_or_subprocess_call::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
pub(crate) use sleep_forever_call::*;
pub(crate) use sync_call::*;
pub(crate) use timeout_without_await::*;
pub(crate) use unneeded_sleep::*;
pub(crate) use zero_sleep_call::*;
mod async_function_with_timeout;
mod blocking_http_call;
mod blocking_os_call;
mod open_sleep_or_subprocess_call;
mod blocking_open_call;
mod blocking_process_invocation;
mod blocking_sleep;
mod sleep_forever_call;
mod sync_call;
mod timeout_without_await;
mod unneeded_sleep;
mod zero_sleep_call;

View File

@@ -6,7 +6,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::rules::flake8_trio::method_name::MethodName;
use crate::rules::flake8_async::helpers::MethodName;
/// ## What it does
/// Checks for calls to trio functions that are not immediately awaited.
@@ -50,7 +50,7 @@ impl Violation for TrioSyncCall {
}
}
/// TRIO105
/// ASYNC105
pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;

View File

@@ -6,7 +6,7 @@ use ruff_python_ast::{StmtWith, WithItem};
use ruff_python_semantic::Modules;
use crate::checkers::ast::Checker;
use crate::rules::flake8_trio::method_name::MethodName;
use crate::rules::flake8_async::helpers::MethodName;
/// ## What it does
/// Checks for trio functions that should contain await but don't.
@@ -44,7 +44,7 @@ impl Violation for TrioTimeoutWithoutAwait {
}
}
/// TRIO100
/// ASYNC100
pub(crate) fn timeout_without_await(
checker: &mut Checker,
with_stmt: &StmtWith,

View File

@@ -41,7 +41,7 @@ impl Violation for TrioUnneededSleep {
}
}
/// TRIO110
/// ASYNC110
pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;

View File

@@ -45,7 +45,7 @@ impl AlwaysFixableViolation for TrioZeroSleepCall {
}
}
/// TRIO115
/// ASYNC115
pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
if !checker.semantic().seen_module(Modules::TRIO) {
return;

View File

@@ -1,39 +1,20 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC100.py:7:5: ASYNC100 Async functions should not call blocking HTTP methods
ASYNC100.py:5:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
6 | async def foo():
7 | urllib.request.urlopen("http://example.com/foo/bar").read()
| ^^^^^^^^^^^^^^^^^^^^^^ ASYNC100
4 | async def func():
5 | with trio.fail_after():
| _____^
6 | | ...
| |___________^ ASYNC100
|
ASYNC100.py:11:5: ASYNC100 Async functions should not call blocking HTTP methods
ASYNC100.py:15:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
10 | async def foo():
11 | requests.get()
| ^^^^^^^^^^^^ ASYNC100
14 | async def func():
15 | with trio.move_on_after():
| _____^
16 | | ...
| |___________^ ASYNC100
|
ASYNC100.py:15:5: ASYNC100 Async functions should not call blocking HTTP methods
|
14 | async def foo():
15 | httpx.get()
| ^^^^^^^^^ ASYNC100
|
ASYNC100.py:19:5: ASYNC100 Async functions should not call blocking HTTP methods
|
18 | async def foo():
19 | requests.post()
| ^^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:23:5: ASYNC100 Async functions should not call blocking HTTP methods
|
22 | async def foo():
23 | httpx.post()
| ^^^^^^^^^^ ASYNC100
|

View File

@@ -1,84 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC101.py:10:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
9 | async def func():
10 | open("foo")
| ^^^^ ASYNC101
|
ASYNC101.py:14:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
13 | async def func():
14 | time.sleep(1)
| ^^^^^^^^^^ ASYNC101
|
ASYNC101.py:18:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
17 | async def func():
18 | subprocess.run("foo")
| ^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:22:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
21 | async def func():
22 | subprocess.call("foo")
| ^^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:30:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
29 | async def func():
30 | os.wait4(10)
| ^^^^^^^^ ASYNC101
|
ASYNC101.py:34:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
33 | async def func():
34 | os.wait(12)
| ^^^^^^^ ASYNC101
|
ASYNC101.py:41:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
40 | async def func():
41 | Path("foo").open() # ASYNC101
| ^^^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:46:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
44 | async def func():
45 | p = Path("foo")
46 | p.open() # ASYNC101
| ^^^^^^ ASYNC101
|
ASYNC101.py:50:10: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
49 | async def func():
50 | with Path("foo").open() as f: # ASYNC101
| ^^^^^^^^^^^^^^^^ ASYNC101
51 | pass
|
ASYNC101.py:58:9: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
57 | async def bar():
58 | p.open() # ASYNC101
| ^^^^^^ ASYNC101
|
ASYNC101.py:64:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
62 | (p1, p2) = (Path("foo"), Path("bar"))
63 |
64 | p1.open() # ASYNC101
| ^^^^^^^ ASYNC101
|

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC102.py:5:5: ASYNC102 Async functions should not call synchronous `os` methods
|
4 | async def foo():
5 | os.popen()
| ^^^^^^^^ ASYNC102
|
ASYNC102.py:9:5: ASYNC102 Async functions should not call synchronous `os` methods
|
8 | async def foo():
9 | os.spawnl()
| ^^^^^^^^^ ASYNC102
|

View File

@@ -1,11 +1,11 @@
---
source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
TRIO105.py:30:5: TRIO105 [*] Call to `trio.aclose_forcefully` is not immediately awaited
ASYNC105.py:30:5: ASYNC105 [*] Call to `trio.aclose_forcefully` is not immediately awaited
|
29 | # TRIO105
29 | # ASYNC105
30 | trio.aclose_forcefully(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
|
@@ -14,19 +14,19 @@ TRIO105.py:30:5: TRIO105 [*] Call to `trio.aclose_forcefully` is not immediately
Unsafe fix
27 27 | await trio.lowlevel.wait_writable(foo)
28 28 |
29 29 | # TRIO105
29 29 | # ASYNC105
30 |- trio.aclose_forcefully(foo)
30 |+ await trio.aclose_forcefully(foo)
31 31 | trio.open_file(foo)
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
TRIO105.py:31:5: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
ASYNC105.py:31:5: ASYNC105 [*] Call to `trio.open_file` is not immediately awaited
|
29 | # TRIO105
29 | # ASYNC105
30 | trio.aclose_forcefully(foo)
31 | trio.open_file(foo)
| ^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^ ASYNC105
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
|
@@ -34,7 +34,7 @@ TRIO105.py:31:5: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
Unsafe fix
28 28 |
29 29 | # TRIO105
29 29 | # ASYNC105
30 30 | trio.aclose_forcefully(foo)
31 |- trio.open_file(foo)
31 |+ await trio.open_file(foo)
@@ -42,19 +42,19 @@ TRIO105.py:31:5: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
TRIO105.py:32:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_listeners` is not immediately awaited
ASYNC105.py:32:5: ASYNC105 [*] Call to `trio.open_ssl_over_tcp_listeners` is not immediately awaited
|
30 | trio.aclose_forcefully(foo)
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
|
= help: Add `await`
Unsafe fix
29 29 | # TRIO105
29 29 | # ASYNC105
30 30 | trio.aclose_forcefully(foo)
31 31 | trio.open_file(foo)
32 |- trio.open_ssl_over_tcp_listeners(foo, foo)
@@ -63,12 +63,12 @@ TRIO105.py:32:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_listeners` is not i
34 34 | trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
TRIO105.py:33:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_stream` is not immediately awaited
ASYNC105.py:33:5: ASYNC105 [*] Call to `trio.open_ssl_over_tcp_stream` is not immediately awaited
|
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
|
@@ -84,12 +84,12 @@ TRIO105.py:33:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_stream` is not imme
35 35 | trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
TRIO105.py:34:5: TRIO105 [*] Call to `trio.open_tcp_listeners` is not immediately awaited
ASYNC105.py:34:5: ASYNC105 [*] Call to `trio.open_tcp_listeners` is not immediately awaited
|
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
|
@@ -105,12 +105,12 @@ TRIO105.py:34:5: TRIO105 [*] Call to `trio.open_tcp_listeners` is not immediatel
36 36 | trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
TRIO105.py:35:5: TRIO105 [*] Call to `trio.open_tcp_stream` is not immediately awaited
ASYNC105.py:35:5: ASYNC105 [*] Call to `trio.open_tcp_stream` is not immediately awaited
|
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
|
@@ -126,12 +126,12 @@ TRIO105.py:35:5: TRIO105 [*] Call to `trio.open_tcp_stream` is not immediately a
37 37 | trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
TRIO105.py:36:5: TRIO105 [*] Call to `trio.open_unix_socket` is not immediately awaited
ASYNC105.py:36:5: ASYNC105 [*] Call to `trio.open_unix_socket` is not immediately awaited
|
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
|
@@ -147,12 +147,12 @@ TRIO105.py:36:5: TRIO105 [*] Call to `trio.open_unix_socket` is not immediately
38 38 | trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
TRIO105.py:37:5: TRIO105 [*] Call to `trio.run_process` is not immediately awaited
ASYNC105.py:37:5: ASYNC105 [*] Call to `trio.run_process` is not immediately awaited
|
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
| ^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^ ASYNC105
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
|
@@ -168,12 +168,12 @@ TRIO105.py:37:5: TRIO105 [*] Call to `trio.run_process` is not immediately await
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
TRIO105.py:38:5: TRIO105 [*] Call to `trio.serve_listeners` is not immediately awaited
ASYNC105.py:38:5: ASYNC105 [*] Call to `trio.serve_listeners` is not immediately awaited
|
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
|
@@ -189,12 +189,12 @@ TRIO105.py:38:5: TRIO105 [*] Call to `trio.serve_listeners` is not immediately a
40 40 | trio.serve_tcp(foo, foo)
41 41 | trio.sleep(foo)
TRIO105.py:39:5: TRIO105 [*] Call to `trio.serve_ssl_over_tcp` is not immediately awaited
ASYNC105.py:39:5: ASYNC105 [*] Call to `trio.serve_ssl_over_tcp` is not immediately awaited
|
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
|
@@ -210,12 +210,12 @@ TRIO105.py:39:5: TRIO105 [*] Call to `trio.serve_ssl_over_tcp` is not immediatel
41 41 | trio.sleep(foo)
42 42 | trio.sleep_forever()
TRIO105.py:40:5: TRIO105 [*] Call to `trio.serve_tcp` is not immediately awaited
ASYNC105.py:40:5: ASYNC105 [*] Call to `trio.serve_tcp` is not immediately awaited
|
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
41 | trio.sleep(foo)
42 | trio.sleep_forever()
|
@@ -231,12 +231,12 @@ TRIO105.py:40:5: TRIO105 [*] Call to `trio.serve_tcp` is not immediately awaited
42 42 | trio.sleep_forever()
43 43 | trio.sleep_until(foo)
TRIO105.py:41:5: TRIO105 [*] Call to `trio.sleep` is not immediately awaited
ASYNC105.py:41:5: ASYNC105 [*] Call to `trio.sleep` is not immediately awaited
|
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
| ^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^ ASYNC105
42 | trio.sleep_forever()
43 | trio.sleep_until(foo)
|
@@ -252,12 +252,12 @@ TRIO105.py:41:5: TRIO105 [*] Call to `trio.sleep` is not immediately awaited
43 43 | trio.sleep_until(foo)
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
TRIO105.py:42:5: TRIO105 [*] Call to `trio.sleep_forever` is not immediately awaited
ASYNC105.py:42:5: ASYNC105 [*] Call to `trio.sleep_forever` is not immediately awaited
|
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
42 | trio.sleep_forever()
| ^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^ ASYNC105
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
|
@@ -273,12 +273,12 @@ TRIO105.py:42:5: TRIO105 [*] Call to `trio.sleep_forever` is not immediately awa
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
45 45 | trio.lowlevel.checkpoint()
TRIO105.py:44:5: TRIO105 [*] Call to `trio.lowlevel.cancel_shielded_checkpoint` is not immediately awaited
ASYNC105.py:44:5: ASYNC105 [*] Call to `trio.lowlevel.cancel_shielded_checkpoint` is not immediately awaited
|
42 | trio.sleep_forever()
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
|
@@ -294,12 +294,12 @@ TRIO105.py:44:5: TRIO105 [*] Call to `trio.lowlevel.cancel_shielded_checkpoint`
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
TRIO105.py:45:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint` is not immediately awaited
ASYNC105.py:45:5: ASYNC105 [*] Call to `trio.lowlevel.checkpoint` is not immediately awaited
|
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
45 | trio.lowlevel.checkpoint()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
|
@@ -315,12 +315,12 @@ TRIO105.py:45:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint` is not immediate
47 47 | trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
TRIO105.py:46:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint_if_cancelled` is not immediately awaited
ASYNC105.py:46:5: ASYNC105 [*] Call to `trio.lowlevel.checkpoint_if_cancelled` is not immediately awaited
|
44 | trio.lowlevel.cancel_shielded_checkpoint()
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
|
@@ -336,12 +336,12 @@ TRIO105.py:46:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint_if_cancelled` is
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
TRIO105.py:47:5: TRIO105 [*] Call to `trio.lowlevel.open_process` is not immediately awaited
ASYNC105.py:47:5: ASYNC105 [*] Call to `trio.lowlevel.open_process` is not immediately awaited
|
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
|
@@ -357,12 +357,12 @@ TRIO105.py:47:5: TRIO105 [*] Call to `trio.lowlevel.open_process` is not immedia
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
TRIO105.py:48:5: TRIO105 [*] Call to `trio.lowlevel.permanently_detach_coroutine_object` is not immediately awaited
ASYNC105.py:48:5: ASYNC105 [*] Call to `trio.lowlevel.permanently_detach_coroutine_object` is not immediately awaited
|
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
|
@@ -378,12 +378,12 @@ TRIO105.py:48:5: TRIO105 [*] Call to `trio.lowlevel.permanently_detach_coroutine
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
TRIO105.py:49:5: TRIO105 [*] Call to `trio.lowlevel.reattach_detached_coroutine_object` is not immediately awaited
ASYNC105.py:49:5: ASYNC105 [*] Call to `trio.lowlevel.reattach_detached_coroutine_object` is not immediately awaited
|
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
|
@@ -399,12 +399,12 @@ TRIO105.py:49:5: TRIO105 [*] Call to `trio.lowlevel.reattach_detached_coroutine_
51 51 | trio.lowlevel.wait_readable(foo)
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
TRIO105.py:50:5: TRIO105 [*] Call to `trio.lowlevel.temporarily_detach_coroutine_object` is not immediately awaited
ASYNC105.py:50:5: ASYNC105 [*] Call to `trio.lowlevel.temporarily_detach_coroutine_object` is not immediately awaited
|
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
|
@@ -420,12 +420,12 @@ TRIO105.py:50:5: TRIO105 [*] Call to `trio.lowlevel.temporarily_detach_coroutine
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
53 53 | trio.lowlevel.wait_writable(foo)
TRIO105.py:51:5: TRIO105 [*] Call to `trio.lowlevel.wait_readable` is not immediately awaited
ASYNC105.py:51:5: ASYNC105 [*] Call to `trio.lowlevel.wait_readable` is not immediately awaited
|
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
52 | trio.lowlevel.wait_task_rescheduled(foo)
53 | trio.lowlevel.wait_writable(foo)
|
@@ -441,12 +441,12 @@ TRIO105.py:51:5: TRIO105 [*] Call to `trio.lowlevel.wait_readable` is not immedi
53 53 | trio.lowlevel.wait_writable(foo)
54 54 |
TRIO105.py:52:5: TRIO105 [*] Call to `trio.lowlevel.wait_task_rescheduled` is not immediately awaited
ASYNC105.py:52:5: ASYNC105 [*] Call to `trio.lowlevel.wait_task_rescheduled` is not immediately awaited
|
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
53 | trio.lowlevel.wait_writable(foo)
|
= help: Add `await`
@@ -461,12 +461,12 @@ TRIO105.py:52:5: TRIO105 [*] Call to `trio.lowlevel.wait_task_rescheduled` is no
54 54 |
55 55 | async with await trio.open_file(foo): # Ok
TRIO105.py:53:5: TRIO105 [*] Call to `trio.lowlevel.wait_writable` is not immediately awaited
ASYNC105.py:53:5: ASYNC105 [*] Call to `trio.lowlevel.wait_writable` is not immediately awaited
|
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
53 | trio.lowlevel.wait_writable(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC105
54 |
55 | async with await trio.open_file(foo): # Ok
|
@@ -482,12 +482,12 @@ TRIO105.py:53:5: TRIO105 [*] Call to `trio.lowlevel.wait_writable` is not immedi
55 55 | async with await trio.open_file(foo): # Ok
56 56 | pass
TRIO105.py:58:16: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
ASYNC105.py:58:16: ASYNC105 [*] Call to `trio.open_file` is not immediately awaited
|
56 | pass
57 |
58 | async with trio.open_file(foo): # TRIO105
| ^^^^^^^^^^^^^^^^^^^ TRIO105
58 | async with trio.open_file(foo): # ASYNC105
| ^^^^^^^^^^^^^^^^^^^ ASYNC105
59 | pass
|
= help: Add `await`
@@ -496,19 +496,17 @@ TRIO105.py:58:16: TRIO105 [*] Call to `trio.open_file` is not immediately awaite
55 55 | async with await trio.open_file(foo): # Ok
56 56 | pass
57 57 |
58 |- async with trio.open_file(foo): # TRIO105
58 |+ async with await trio.open_file(foo): # TRIO105
58 |- async with trio.open_file(foo): # ASYNC105
58 |+ async with await trio.open_file(foo): # ASYNC105
59 59 | pass
60 60 |
61 61 |
TRIO105.py:64:5: TRIO105 Call to `trio.open_file` is not immediately awaited
ASYNC105.py:64:5: ASYNC105 Call to `trio.open_file` is not immediately awaited
|
62 | def func() -> None:
63 | # TRIO105 (without fix)
63 | # ASYNC105 (without fix)
64 | trio.open_file(foo)
| ^^^^^^^^^^^^^^^^^^^ TRIO105
| ^^^^^^^^^^^^^^^^^^^ ASYNC105
|
= help: Add `await`

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