Compare commits

...

87 Commits

Author SHA1 Message Date
Micha Reiser
09c50c311c Testing Macros: Add extra-traits feature (#4643) 2023-05-24 17:14:58 +00:00
Charlie Marsh
252506f8ed Remove deprecated --universal2 flag (#4640) 2023-05-24 17:00:52 +00:00
Charlie Marsh
f4572fe40b Bump version to 0.0.270 (#4637) 2023-05-24 16:34:29 +00:00
Sladyn
8c9215489e Migrate flake8_bugbear rules to unspecified to suggested (#4616) 2023-05-24 16:16:33 +00:00
qdegraaf
dcd2bfaab7 Migrate flake8_pie autofix rules from unspecified to suggested and automatic (#4621) 2023-05-24 16:08:22 +00:00
Charlie Marsh
f0e173d9fd Use BindingId copies in lieu of &BindingId in semantic model methods (#4633) 2023-05-24 15:55:45 +00:00
Charlie Marsh
f4f1b1d0ee Only run the playground release job on release (#4636) 2023-05-24 11:48:36 -04:00
Micha Reiser
edc6c4058f Move shared_traits to ruff_formatter (#4632) 2023-05-24 17:38:11 +02:00
Jonathan Plasse
4233f6ec91 Update to the new rule architecture (#4589) 2023-05-24 11:30:40 -04:00
Charlie Marsh
fcdc7bdd33 Remove separate ReferenceContext enum (#4631) 2023-05-24 15:12:38 +00:00
Micha Reiser
86ced3516b Introduce SourceCodeSlice to reduce the size of FormatElement (#4622)
Introduce `SourceCodeSlice` to reduce the size of `FormatElement`
2023-05-24 15:04:52 +00:00
Micha Reiser
6943beee66 Remove source position from FormatElement::DynamicText (#4619) 2023-05-24 16:36:14 +02:00
Micha Reiser
85f094f592 Improve Message sorting performance (#4624) 2023-05-24 16:34:48 +02:00
konstin
17d938f078 Add Checker::any_enabled shortcut (#4630)
Add Checker::any_enabled shortcut

 ## Summary

 Akin to #4625, This is a refactoring that shortens a bunch of code by replacing `checker.settings.rules.any_enabled` with `checker.any_enabled`.

 ## Test Plan

 `cargo clippy`
2023-05-24 14:32:55 +00:00
Charlie Marsh
5cedf0f724 Remove ReferenceContext::Synthetic (#4612) 2023-05-24 14:30:35 +00:00
konstin
38297c08b4 Make ecosystem all check more generic (#4629)
* Don't assume unique repo names in ecosystem checks

This fixes a bug where previously repositories with the same name would have been overwritten.

I tested with `scripts/check_ecosystem.py -v --checkouts target/checkouts_main .venv/bin/ruff target/release/ruff` and ruff 0.0.267 that changes are shown. I confirmed with `scripts/ecosystem_all_check.sh check --select RUF008` (next PR) that the checkouts are now complete.

* Make ecosystem all check more generic

This allows passing arguments to the ecosystem all check script, e.g. you can now do `scripts/ecosystem_all_check.sh check --select RUF008`.

Tested with
```
$ cat target/ecosystem_all_results/*.stdout.txt | head
src/fi_parliament_tools/parsing/data_structures.py:33:17: RUF008 Do not use mutable default values for dataclass attributes
src/fi_parliament_tools/parsing/data_structures.py:76:17: RUF008 Do not use mutable default values for dataclass attributes
src/fi_parliament_tools/parsing/data_structures.py:178:17: RUF008 Do not use mutable default values for dataclass attributes
Found 3 errors.
braid_triggers/tasks.py:46:17: RUF008 Do not use mutable default values for dataclass attributes
Found 1 error.
src/boards/RaspberryPi3.py:15:22: RUF008 Do not use mutable default values for dataclass attributes
src/boards/board.py:21:26: RUF008 Do not use mutable default values for dataclass attributes
src/boards/board.py:22:32: RUF008 Do not use mutable default values for dataclass attributes
src/boards/board.py:23:37: RUF008 Do not use mutable default values for dataclass attributes
$ cat target/ecosystem_all_results/*.stdout.txt | wc -l
115
```
2023-05-24 16:26:23 +02:00
konstin
30e90838d0 Don't assume unique repo names in ecosystem checks (#4628)
This fixes a bug where previously repositories with the same name would have been overwritten.

I tested with `scripts/check_ecosystem.py -v --checkouts target/checkouts_main .venv/bin/ruff target/release/ruff` and ruff 0.0.267 that changes are shown. I confirmed with `scripts/ecosystem_all_check.sh check --select RUF008` (next PR) that the checkouts are now complete.
2023-05-24 16:26:12 +02:00
Charlie Marsh
040fb9cef4 Use a separate PrinterFlag for including fix diffs (#4615) 2023-05-24 10:22:37 -04:00
Charlie Marsh
8961d8eb6f Track all read references in semantic model (#4610) 2023-05-24 14:14:27 +00:00
Charlie Marsh
31bddef98f Visit TypeVar and NewType name arguments (#4627) 2023-05-24 10:10:15 -04:00
konstin
a59d252246 Add Checker::enabled shortcut (#4625)
This is a refactoring that shortens a bunch of code by replacing `checker.settings.rules.enabled` with `checker.enabled`
2023-05-24 14:56:41 +02:00
konstin
5b9d4f18ae Remove outdated feature flag from Dockerfile.ecosystem (#4620) 2023-05-24 08:19:08 +00:00
Jonathan Plasse
c6a760e298 Introduce tab-size to correcly calculate the line length with tabulations (#4167) 2023-05-24 08:37:24 +02:00
konstin
3644695bf2 Include hidden ecosystem_ci option to show fixes without feature (#4528) 2023-05-23 22:22:23 -04:00
Evan Rittenhouse
b1d01b1950 Add a PR template (#4582) 2023-05-24 02:17:38 +00:00
Sladyn
4e84e8a8e2 Migrate some rules from Fix::unspecified (#4587) 2023-05-23 22:10:58 -04:00
Hoël Bagard
a256fdb9f4 Extend RUF005 to recursive and literal-literal concatenations (#4557) 2023-05-24 01:26:34 +00:00
Tom Kuson
7479dfd815 Add Pyflakes docs (#4588) 2023-05-24 00:45:32 +00:00
Charlie Marsh
ba4c0a21fa Rename ContextFlags to SemanticModelFlags (#4611) 2023-05-23 17:47:07 -04:00
konstin
73e179ffab Update maturin to 1.0 (#4605)
* Refactor and fix task trigger for dependent jobs in other repos

I have confirmed (https://github.com/konstin/ruff-pre-commit/actions/runs/5056928280/jobs/9075029868) that this does dispatch the workflow when running with act, `owner: 'konstin'`, `needs` commented out and personal access token. I can't properly test the actual release workflow, and i'm unsure how to best handle the next release after this was merged (should we do a beta release or will this break everything that assumes we only do stable releases?)

The command for act is
```
act -j update-dependents -s RUFF_PRE_COMMIT_PAT=<...>
```

* delete old file

* Update maturin to 1.0

A 1.0 release for maturin 🎉
2023-05-23 20:55:52 +02:00
konstin
3cbaaa4795 Refactor and fix task trigger for dependent jobs in other repos (#4598)
* Refactor and fix task trigger for dependent jobs in other repos

I have confirmed (https://github.com/konstin/ruff-pre-commit/actions/runs/5056928280/jobs/9075029868) that this does dispatch the workflow when running with act, `owner: 'konstin'`, `needs` commented out and personal access token. I can't properly test the actual release workflow, and i'm unsure how to best handle the next release after this was merged (should we do a beta release or will this break everything that assumes we only do stable releases?)

The command for act is
```
act -j update-dependents -s RUFF_PRE_COMMIT_PAT=<...>
```

* delete old file
2023-05-23 20:55:35 +02:00
Micha Reiser
2681c0e633 Add missing nodes to AnyNodeRef and AnyNode (#4608) 2023-05-23 18:30:27 +02:00
Charlie Marsh
f3bdd2e7be Make B007 fix relevance stricter (#4607) 2023-05-23 15:43:59 +00:00
Micha Reiser
652c644c2a Introduce ruff_index crate (#4597) 2023-05-23 17:40:35 +02:00
konstin
04d273bcc7 Add a script to update the schemastore (#4574)
* Add a script to update the schemastore

Hacked this together, it clones astral-sh/schemastore, updated the schema and pushes the changes
to a new branch tagged with the ruff git hash. You can see the URL to create the PR
to schemastore in the CLI. The script is separated into three blocks so you can rerun
the schema generation in the middle before committing.

* Use tempdir for schemastore

* Add comments
2023-05-23 10:41:56 +00:00
Micha Reiser
154439728a Add AnyNode and AnyNodeRef unions (#4578) 2023-05-23 08:53:22 +02:00
Jonathan Plasse
1ddc577204 Rework CST matchers (#4536)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-05-23 06:26:51 +00:00
Charlie Marsh
74effb40b9 Rename index to binding_id in a few iterators (#4594) 2023-05-23 03:56:00 +00:00
Charlie Marsh
6c3724ab98 Move get_or_import_symbol onto Importer (#4591) 2023-05-23 01:33:00 +00:00
Christopher Covington
3b8121379d Name ambiguous characters (#4448) 2023-05-22 17:16:57 +00:00
qdegraaf
5ba47c3302 Add autofix for PYI009 (#4583) 2023-05-22 16:41:18 +00:00
Charlie Marsh
b613460fe5 Fix # isort: split comment detection in nested blocks (#4584) 2023-05-22 12:31:59 -04:00
Micha Reiser
daadd24bde Include decorators in Function and Class definition ranges (#4467) 2023-05-22 17:50:42 +02:00
Charlie Marsh
9308e939f4 Avoid infinite loop for required imports with isort: off (#4581) 2023-05-22 15:49:03 +00:00
Charlie Marsh
04c9348de0 Make ambiguous-unicode detection sensitive to 'word' context (#4552) 2023-05-22 14:42:25 +00:00
Tom Kuson
2d3766d928 Add flake8-boolean-trap docs (#4572) 2023-05-22 14:11:14 +00:00
konstin
550b643e33 Add script for ecosystem wide checks of all rules and fixes (#4326)
* Add script for ecosystem wide checks of all rules and fixes

This adds my personal script for checking an entire checkout of ~2.1k packages for
panics, autofix errors and similar problems. It's not really meant to be used by anybody else but i thought it's better if it lives in the repo than if it doesn't.

For reference, this is the current output of failing autofixes: https://gist.github.com/konstin/c3fada0135af6cacec74f166adf87a00. Trimmed down to the useful information: https://gist.github.com/konstin/c864f4c300c7903a24fdda49635c5da9

* Keep github template intact

* Remove the need for ripgrep

* sort output
2023-05-22 15:23:25 +02:00
Micha Reiser
cbe344f4d5 Rename Checker::model to semantic_model (#4573) 2023-05-22 15:14:30 +02:00
Micha Reiser
063431cb0f Upgrade RustPython (#4576) 2023-05-22 14:50:49 +02:00
Evan Rittenhouse
c6e5fed658 Replace token iteration with Indexer/Locator lookups for relevant rules (#4513) 2023-05-22 09:56:19 +02:00
Charlie Marsh
f73b398776 Reduce visibility of more functions, structs, and fields (#4570) 2023-05-22 03:36:48 +00:00
Charlie Marsh
55c4020ba9 Remove regex for noqa code splitting (#4569) 2023-05-21 23:20:49 -04:00
Charlie Marsh
d70f899f71 Use SemanticModel in lieu of Checker in more methods (#4568) 2023-05-22 02:58:47 +00:00
Charlie Marsh
19c4b7bee6 Rename ruff_python_semantic's Context struct to SemanticModel (#4565) 2023-05-22 02:35:03 +00:00
Jonathan Plasse
3238743a7b Fix Flake8Todo typo (#4566) 2023-05-21 16:32:13 -04:00
Charlie Marsh
f22c269ccf Point LSP, VS Code, and pre-commut URLs to Astral org (#4562) 2023-05-21 15:27:35 -04:00
Arne de Laat
8ca3977602 Fix false-positive for TRY302 if exception cause is given (#4559) 2023-05-21 11:49:53 -04:00
Jacob Coffee
6db05d8cc6 Starlite -> Litestar (#4554) 2023-05-21 09:55:26 -04:00
Jonathan Plasse
fc63c6f2e2 Fix PLE01310 typo (#4550) 2023-05-20 19:34:03 +00:00
Jonathan Plasse
f7f5bc9085 Fix SIM401 snapshot (#4547) 2023-05-20 14:18:19 -04:00
Charlie Marsh
6b85430a14 Ignore #region code folding marks in eradicate rules (#4546) 2023-05-20 16:45:49 +00:00
Jonathan Plasse
a68c865010 Fix SIM110 and SIM111 ranges (#4545) 2023-05-20 12:40:35 -04:00
Charlie Marsh
fe7f2e2e4d Move submodule alias resolution into Context (#4543) 2023-05-20 16:34:10 +00:00
Felipe Peter
0a3cf8ba11 Fix typos in docs (#4540) 2023-05-20 07:23:17 -04:00
Charlie Marsh
bf5b463c0d Include empty success test in JUnit output (#4537) 2023-05-20 03:38:51 +00:00
Charlie Marsh
6aa9900c03 Improve handling of __qualname__, __module__, and __class__ (#4512) 2023-05-20 03:03:45 +00:00
Charlie Marsh
9e21414294 Improve reference resolution for deferred-annotations-within-classes (#4509) 2023-05-20 02:54:18 +00:00
Charlie Marsh
bb4e674415 Move reference-resolution into Context (#4510) 2023-05-20 02:47:15 +00:00
Charlie Marsh
b42ff08612 Parenthesize more sub-expressions in f-string conversion (#4535) 2023-05-19 19:41:30 +00:00
Jonathan Plasse
03fb62c174 Fix RUF010 auto-fix with parenthesis (#4524) 2023-05-19 19:05:51 +00:00
Jonathan Plasse
2dfc645ea9 Fix UP032 auto-fix with integers (#4525) 2023-05-19 18:53:50 +00:00
Hoël Bagard
fe8e2bb237 [pylint] Add named_expr_without_context (W0131) (#4531) 2023-05-19 18:00:01 +00:00
Tom Kuson
a9ed8d5391 Add Pylint docs (#4530) 2023-05-19 17:40:18 +00:00
Aaron Cunningham
41a681531d Support new extend-per-file-ignores setting (#4265) 2023-05-19 12:24:04 -04:00
Justin Prieto
837e70677b [flake8-pyi] Implement PYI013 (#4517) 2023-05-19 15:39:55 +00:00
Hoël Bagard
7ebe372122 [pylint] Add duplicate-value (W0130) (#4515) 2023-05-19 15:03:47 +00:00
konstin
625849b846 Ecosystem CI: Optionally diff fixes (#4193)
* Generate fixes when using --show-fixes

Example command: `cargo run --bin ruff -- --no-cache --select F401
--show-source --show-fixes
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py`

Before, `--show-fixes` was ignored:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

After:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

ℹ Suggested fix
1 1 | """Test: late-binding of `__all__`."""
2 2 |
3 3 | __all__ = ("bar",)
4   |-from foo import bar, baz
  4 |+from foo import bar

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

Also fixes git clone
2023-05-19 09:49:57 +00:00
konstin
32f1edc555 Create dummy format CLI (#4453)
* Create dummy format CLI

* Hide format from clap, too

Missed that this is a separate option from `#[doc(hidden)]`

* Remove cargo feature and replace with warning

* No-alloc files parameter matching

* beta warning: warn -> warn_user_once

* Rephrase warning
2023-05-19 11:45:52 +02:00
Micha Reiser
2f35099f81 Remove regex dependency from ruff_python_ast (#4518) 2023-05-19 06:44:18 +00:00
Hoël Bagard
ce8fd31a8f Updated contributing documentation (#4516) 2023-05-19 08:39:15 +02:00
Ville Skyttä
fdb241cad2 [flake8-bandit] Implement paramiko-call (S601) (#4500) 2023-05-19 03:40:50 +00:00
Charlie Marsh
ab303f4e09 Gate schemars skip under feature flag (#4514) 2023-05-19 03:01:31 +00:00
Charlie Marsh
15cb21a6f4 Implement --extend-fixable option (#4297) 2023-05-18 22:20:19 -04:00
Ville Skyttä
2e2ba2cb16 Avoid some false positives in dunder variable assigments (#4508) 2023-05-19 02:11:20 +00:00
Charlie Marsh
d4c0a41b00 Bump version to 0.0.269 (#4506) 2023-05-18 19:45:20 +00:00
Charlie Marsh
8702b5a40a Bump version to 0.0.268 (#4501) 2023-05-18 15:35:46 -04:00
figsoda
bab818e801 Update RustPython dependencies (#4503) 2023-05-18 15:28:13 -04:00
621 changed files with 17058 additions and 8305 deletions

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,15 @@
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->
## Summary
<!-- What's the purpose of the change? What does it do, and why? -->
## Test Plan
<!-- How was it tested? -->

View File

@@ -183,18 +183,8 @@ jobs:
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
run: |
unused_dependencies=$(cargo +nightly-2023-03-30 udeps > unused.txt && cat unused.txt | cut -d $'\n' -f 2-)
if [ -z "$unused_dependencies" ]; then
echo "No unused dependencies found" > $GITHUB_STEP_SUMMARY
exit 0
else
echo "Found unused dependencies" > $GITHUB_STEP_SUMMARY
echo '```console' >> $GITHUB_STEP_SUMMARY
echo "$unused_dependencies" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
run: cargo +nightly-2023-03-30 udeps
python-package:
name: "python package"

View File

@@ -1,9 +1,9 @@
name: mkdocs
on:
release:
types: [published]
workflow_dispatch:
release:
types: [ published ]
jobs:
mkdocs:

View File

@@ -52,7 +52,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --universal2 --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
args: --release --target universal2-apple-darwin --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel - universal2"
run: |
pip install dist/${{ env.CRATE_NAME }}-*universal2.whl --force-reinstall

View File

@@ -2,8 +2,8 @@ name: "[Playground] Release"
on:
workflow_dispatch:
push:
branches: [main]
release:
types: [ published ]
env:
CARGO_INCREMENTAL: 0

View File

@@ -3,7 +3,7 @@ name: "[ruff] Release"
on:
workflow_dispatch:
release:
types: [published]
types: [ published ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -94,7 +94,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --universal2 --out dist
args: --release --target universal2-apple-darwin --out dist
- name: "Test wheel - universal2"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
@@ -406,9 +406,6 @@ jobs:
run: |
pip install --upgrade twine
twine upload --skip-existing *
- name: "Update pre-commit mirror"
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "pypi_release"}'
- uses: actions/download-artifact@v3
with:
name: binaries
@@ -417,3 +414,22 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: binaries/*
# 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: Release
runs-on: ubuntu-latest
needs: release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v6
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',
})

4
.gitignore vendored
View File

@@ -1,10 +1,10 @@
# Local cache
.ruff_cache
crates/ruff/resources/test/cpython
mkdocs.yml
.overrides
ruff-old
github_search*.jsonl
schemastore
.venv*
###
# Rust.gitignore

View File

@@ -21,6 +21,7 @@ repos:
- --disable
- MD013 # line-length
- MD033 # no-inline-html
- MD041 # first-line-h1
- --
- repo: https://github.com/crate-ci/typos

View File

@@ -134,7 +134,7 @@ Run `cargo dev generate-all` to generate the code for your new fixture. Then run
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test`.
`test_case` macro in the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. Then, run `cargo test`.
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
generated snapshot, then commit the snapshot file alongside the rest of your changes.
@@ -148,7 +148,7 @@ This implies that rule names:
- should state the bad thing being checked for
- should not contain instructions on what you what you should use instead
- should not contain instructions on what you should use instead
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
When re-implementing rules from other linters, this convention is given more importance than

45
Cargo.lock generated
View File

@@ -193,9 +193,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
[[package]]
name = "bstr"
@@ -711,7 +711,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.267"
version = "0.0.270"
dependencies = [
"anyhow",
"clap 4.2.7",
@@ -1723,11 +1723,11 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.267"
version = "0.0.270"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
"bitflags 2.2.1",
"bitflags 2.3.1",
"chrono",
"clap 4.2.7",
"colored",
@@ -1780,6 +1780,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-width",
"unicode_names2",
]
[[package]]
@@ -1812,7 +1813,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.267"
version = "0.0.270"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1820,7 +1821,7 @@ dependencies = [
"assert_cmd",
"atty",
"bincode",
"bitflags 2.2.1",
"bitflags 2.3.1",
"cachedir",
"chrono",
"clap 4.2.7",
@@ -1899,10 +1900,19 @@ dependencies = [
"rustc-hash",
"schemars",
"serde",
"static_assertions",
"tracing",
"unicode-width",
]
[[package]]
name = "ruff_index"
version = "0.0.0"
dependencies = [
"ruff_macros",
"static_assertions",
]
[[package]]
name = "ruff_macros"
version = "0.0.0"
@@ -1919,7 +1929,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.2.1",
"bitflags 2.3.1",
"is-macro",
"itertools",
"log",
@@ -1927,9 +1937,9 @@ dependencies = [
"num-bigint",
"num-traits",
"once_cell",
"regex",
"ruff_text_size",
"rustc-hash",
"rustpython-ast",
"rustpython-literal",
"rustpython-parser",
"serde",
@@ -1961,9 +1971,10 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.2.1",
"bitflags 2.3.1",
"is-macro",
"nohash-hasher",
"ruff_index",
"ruff_python_ast",
"ruff_python_stdlib",
"ruff_text_size",
@@ -2001,7 +2012,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"schemars",
"serde",
@@ -2072,7 +2083,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"num-bigint",
@@ -2083,9 +2094,9 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"bitflags 2.2.1",
"bitflags 2.3.1",
"itertools",
"num-bigint",
"num-traits",
@@ -2095,7 +2106,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2107,7 +2118,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"anyhow",
"is-macro",
@@ -2130,7 +2141,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"ruff_text_size",

View File

@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "2.2.1" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
clap = { version = "4.1.8", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -31,10 +31,11 @@ proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
rustc-hash = { version = "1.1.0" }
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["all-nodes-with-ranges"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }

View File

@@ -33,7 +33,7 @@ An extremely fast Python linter, written in Rust.
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party editor integrations for [VS Code](https://github.com/charliermarsh/ruff-vscode) and [more](https://github.com/charliermarsh/ruff-lsp)
- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
@@ -135,15 +135,15 @@ ruff check path/to/code/to/file.py # Lint `file.py`
Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.267'
rev: v0.0.270
hooks:
- id: ruff
```
Ruff can also be used as a [VS Code extension](https://github.com/charliermarsh/ruff-vscode) or
alongside any other editor through the [Ruff LSP](https://github.com/charliermarsh/ruff-lsp).
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or
alongside any other editor through the [Ruff LSP](https://github.com/astral-sh/ruff-lsp).
Ruff can also be used as a [GitHub Action](https://github.com/features/actions) via
[`ruff-action`](https://github.com/chartboost/ruff-action):
@@ -388,7 +388,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [SciPy](https://github.com/scipy/scipy)
- [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
- [Starlite](https://github.com/starlite-api/starlite)
- [Litestar](https://litestar.dev/)
- [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair)
- WordPress ([Openverse](https://github.com/WordPress/openverse))

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.267"
version = "0.0.270"
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -26,7 +26,7 @@ requires-python = ">=3.7"
repository = "https://github.com/charliermarsh/ruff#subdirectory=crates/flake8_to_ruff"
[build-system]
requires = ["maturin>=0.15.2,<0.16"]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.267"
version = "0.0.270"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -70,6 +70,7 @@ thiserror = { version = "1.0.38" }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { version = "0.1.10" }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
[dev-dependencies]
insta = { workspace = true }
@@ -82,4 +83,3 @@ colored = { workspace = true, features = ["no-color"] }
default = []
schemars = ["dep:schemars"]
jupyter_notebook = []
ecosystem_ci = []

View File

@@ -0,0 +1,3 @@
import paramiko
paramiko.exec_command('something; really; unsafe')

View File

@@ -73,7 +73,18 @@ def f():
def f():
# Fixable.
# Unfixable.
for foo, bar, baz in (["1", "2", "3"],):
if foo or baz:
break
else:
bar = 1
print(bar)
def f():
# Unfixable (false negative) due to usage of `bar` outside of loop.
for foo, bar, baz in (["1", "2", "3"],):
if foo or baz:
break
@@ -85,4 +96,4 @@ def f():
# Unfixable due to trailing underscore (`_line_` wouldn't be considered an ignorable
# variable name).
for line_ in range(self.header_lines):
fp.readline()
fp.readline()

View File

@@ -0,0 +1,65 @@
class OneAttributeClass:
value: int
...
class OneAttributeClass2:
...
value: int
class TwoEllipsesClass:
...
...
class DocstringClass:
"""
My body only contains an ellipsis.
"""
...
class NonEmptyChild(Exception):
value: int
...
class NonEmptyChild2(Exception):
...
value: int
class NonEmptyWithInit:
value: int
...
def __init__():
pass
class EmptyClass:
...
class EmptyEllipsis:
...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__():
...
def function():
...
...

View File

@@ -0,0 +1,56 @@
# Violations of PYI013
class OneAttributeClass:
value: int
... # Error
class OneAttributeClass2:
... # Error
value: int
class MyClass:
...
value: int
class TwoEllipsesClass:
...
... # Error
class DocstringClass:
"""
My body only contains an ellipsis.
"""
... # Error
class NonEmptyChild(Exception):
value: int
... # Error
class NonEmptyChild2(Exception):
... # Error
value: int
class NonEmptyWithInit:
value: int
... # Error
def __init__():
pass
# Not violations
class EmptyClass: ...
class EmptyEllipsis: ...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__(): ...
def function(): ...
...

View File

@@ -86,9 +86,16 @@ while x > 0:
):
print("Bad module!")
# SIM102
if node.module:
if node.module == "multiprocessing" or node.module.startswith(
# SIM102 (auto-fixable)
if node.module012345678:
if node.module == "multiprocß9💣2" or node.module.startswith(
"multiprocessing."
):
print("Bad module!")
# SIM102 (not auto-fixable)
if node.module0123456789:
if node.module == "multiprocß9💣2" or node.module.startswith(
"multiprocessing."
):
print("Bad module!")

View File

@@ -80,17 +80,25 @@ else:
# SIM108
if a:
b = cccccccccccccccccccccccccccccccccccc
b = "cccccccccccccccccccccccccccccccccß"
else:
b = ddddddddddddddddddddddddddddddddddddd
b = "ddddddddddddddddddddddddddddddddd💣"
# OK (too long)
if True:
if a:
b = cccccccccccccccccccccccccccccccccccc
b = ccccccccccccccccccccccccccccccccccc
else:
b = ddddddddddddddddddddddddddddddddddddd
b = ddddddddddddddddddddddddddddddddddd
# OK (too long with tabs)
if True:
if a:
b = ccccccccccccccccccccccccccccccccccc
else:
b = ddddddddddddddddddddddddddddddddddd
# SIM108 (without fix due to trailing comment)

View File

@@ -155,3 +155,19 @@ def f():
if check(x):
return False
return True
def f():
# SIM110
for x in "012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣2":
if x.isdigit():
return True
return False
def f():
# OK (too long)
for x in "012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29":
if x.isdigit():
return True
return False

View File

@@ -171,3 +171,19 @@ def f():
if x > y:
return False
return True
def f():
# SIM111
for x in "012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9":
if x.isdigit():
return False
return True
def f():
# OK (too long)
for x in "012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß90":
if x.isdigit():
return False
return True

View File

@@ -90,3 +90,13 @@ with (
D() as d,
):
print("hello")
# SIM117 (auto-fixable)
with A("01ß9💣28901ß9💣28901ß9💣289") as a:
with B("01ß9💣28901ß9💣28901ß9💣289") as b:
print("hello")
# SIM117 (not auto-fixable too long)
with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
with B("01ß9💣28901ß9💣28901ß9💣289") as b:
print("hello")

View File

@@ -14,7 +14,7 @@ if key not in a_dict:
else:
var = a_dict[key]
# SIM401 (default with a complex expression)
# OK (default contains effect)
if key in a_dict:
var = a_dict[key]
else:
@@ -36,12 +36,18 @@ else:
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"
vars[idx] = "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789"
###
# Negative cases
###
# OK (too long)
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789ß"
# OK (false negative)
if not key in a_dict:
var = "default"

View File

@@ -146,3 +146,7 @@ def f():
import pandas as pd
x = dict[pd.DataFrame, pd.DataFrame]
def f():
import pandas as pd

View File

@@ -2,3 +2,7 @@ import a
# Don't take this comment into account when determining whether the next import can fit on one line.
from b import c
from d import e # Do take this comment into account when determining whether the next import can fit on one line.
# The next import fits on one line.
from f import g # 012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣2
# The next import doesn't fit on one line.
from h import i # 012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29012ß9💣29

View File

@@ -0,0 +1,4 @@
# isort: off
x = 1
# isort: on

View File

@@ -6,7 +6,16 @@ import f
import c
import d
# isort: split
# isort: split
import a
import b
if True:
import C
import A
# isort: split
import D
import B

View File

@@ -19,7 +19,7 @@ if x > 0:
else:
import e
y = x + 1
__some__magic = 1
import f

View File

@@ -0,0 +1,11 @@
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Here's a top-level ß9💣2ing that's over theß9💣2."""
def f1():
"""Here's a ß9💣2ing that's also over theß9💣2."""
x = 1 # Here's a comment that's over theß9💣2, but it's not standalone.
# Here's a standalone comment that's over theß9💣2.
x = 2
# Another standalone that is preceded by a newline and indent toke and is over theß9💣2.
print("Here's a string that's over theß9💣2, but it's not a ß9💣2ing.")
"This is also considered a ß9💣2ing, and is over theß9💣2."
def f2():
"""Here's a multi-line ß9💣2ing.
It's over theß9💣2 on this line, which isn't the first line in the ß9💣2ing.
"""
def f3():
"""Here's a multi-line ß9💣2ing.
It's over theß9💣2 on this line, which isn't the first line in the ß9💣2ing."""

View File

@@ -0,0 +1,10 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
from __future__ import annotations
import datetime
from typing import Optional
class Class:
datetime: Optional[datetime.datetime]

View File

@@ -0,0 +1,12 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
from __future__ import annotations
from typing import TypeAlias, List
class Class:
List: TypeAlias = List
def bar(self) -> List:
pass

View File

@@ -0,0 +1,8 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
import datetime
from typing import Optional
class Class:
datetime: "Optional[datetime.datetime]"

View File

@@ -0,0 +1,4 @@
"""Test that shadowing an explicit re-export produces a warning."""
import foo as foo
import bar as foo

View File

@@ -0,0 +1,5 @@
"""Test that shadowing a `__future__` import does not produce a warning."""
from __future__ import annotations
import annotations

View File

@@ -0,0 +1,11 @@
###
# Errors.
###
incorrect_set = {"value1", 23, 5, "value1"}
incorrect_set = {1, 1}
###
# Non-errors.
###
correct_set = {"value1", 23, 5}
correct_set = {5, "5"}

View File

@@ -0,0 +1,19 @@
# Errors
(a := 42)
if True:
(b := 1)
class Foo:
(c := 1)
# OK
if a := 42:
print("Success")
a = 0
while (a := a + 1) < 10:
print("Correct")
a = (b := 1)

View File

@@ -0,0 +1,28 @@
# Errors
"{.real}".format(1)
"{0.real}".format(1)
"{a.real}".format(a=1)
"{.real}".format(1.0)
"{0.real}".format(1.0)
"{a.real}".format(a=1.0)
"{.real}".format(1j)
"{0.real}".format(1j)
"{a.real}".format(a=1j)
"{.real}".format(0b01)
"{0.real}".format(0b01)
"{a.real}".format(a=0b01)
"{}".format(1 + 2)
"{}".format([1, 2])
"{}".format({1, 2})
"{}".format({1: 2, 3: 4})
"{}".format((i for i in range(2)))
"{.real}".format(1 + 2)
"{.real}".format([1, 2])
"{.real}".format({1, 2})
"{.real}".format({1: 2, 3: 4})
"{}".format((i for i in range(2)))

View File

@@ -1,26 +1,10 @@
class Fun:
words = ("how", "fun!")
def yay(self):
return self.words
yay = Fun().yay
foo = [4, 5, 6]
bar = [1, 2, 3] + foo
zoob = tuple(bar)
quux = (7, 8, 9) + zoob
spam = quux + (10, 11, 12)
spom = list(spam)
eggs = spom + [13, 14, 15]
elatement = ("we all say", ) + yay()
excitement = ("we all think", ) + Fun().yay()
astonishment = ("we all feel", ) + Fun.words
chain = ['a', 'b', 'c'] + eggs + list(('yes', 'no', 'pants') + zoob)
baz = () + zoob
###
# Non-fixable Errors.
###
foo + [ # This will be preserved.
]
[*foo] + [ # This will be preserved.
]
first = [
# The order
1, # here
@@ -38,11 +22,47 @@ second = first + [
6,
]
###
# Fixable errors.
###
class Fun:
words = ("how", "fun!")
def yay(self):
return self.words
yay = Fun().yay
foo = [4, 5, 6]
bar = [1, 2, 3] + foo
zoob = tuple(bar)
quux = (7, 8, 9) + zoob
spam = quux + (10, 11, 12)
spom = list(spam)
eggs = spom + [13, 14, 15]
elatement = ("we all say",) + yay()
excitement = ("we all think",) + Fun().yay()
astonishment = ("we all feel",) + Fun.words
chain = ["a", "b", "c"] + eggs + list(("yes", "no", "pants") + zoob)
baz = () + zoob
[] + foo + [
]
[] + foo + [ # This will be preserved, but doesn't prevent the fix
]
pylint_call = [sys.executable, "-m", "pylint"] + args + [path]
pylint_call_tuple = (sys.executable, "-m", "pylint") + args + (path, path2)
b = a + [2, 3] + [4]
# Uses the non-preferred quote style, which should be retained.
f"{[*a(), 'b']}"
f"{a() + ['b']}"
###
# Non-errors.
###
a = (1,) + [2]
a = [1, 2] + (3, 4)
a = ([1, 2, 3] + b) + (4, 5, 6)

View File

@@ -10,6 +10,8 @@ f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010
f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
f"{foo(bla)}" # OK
f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK

View File

@@ -8,7 +8,24 @@ def f():
...
def g():
def f():
"""Here's a docstring with a greek rho: ρ"""
# And here's a comment with a greek alpha:
...
x = "𝐁ad string"
x = ""
# This should be ignored, since it contains an unambiguous unicode character, and no
# ASCII.
x = "Русский"
# The first word should be ignored, while the second should be included, since it
# contains ASCII.
x = "βα Bαd"
# The two characters should be flagged here. The first character is a "word"
# consisting of a single ambiguous character, while the second character is a "word
# boundary" (whitespace) that it itself ambiguous.
x = "Р усский"

View File

@@ -0,0 +1,23 @@
def f():
# These should both be ignored by the `noqa`.
I = 1 # noqa: E741, F841
def f():
# These should both be ignored by the `noqa`.
I = 1 # noqa: E741,F841
def f():
# These should both be ignored by the `noqa`.
I = 1 # noqa: E741 F841
def f():
# These should both be ignored by the `noqa`.
I = 1 # noqa: E741 , F841
def f():
# Only `E741` should be ignored by the `noqa`.
I = 1 # noqa: E741.F841

View File

@@ -68,6 +68,18 @@ def bad():
except Exception as e:
raise e
def fine():
try:
process()
except Exception as e:
raise e from None
def fine():
try:
process()
except Exception as e:
raise e from Exception
def fine():
try:
process()

View File

@@ -10,14 +10,11 @@ use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers;
use ruff_python_ast::imports::{AnyImport, Import};
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_semantic::context::Context;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::importer::Importer;
use crate::cst::matchers::match_statement;
/// Determine if a body contains only a single statement, taking into account
/// deleted.
@@ -215,9 +212,9 @@ pub(crate) fn remove_unused_imports<'a>(
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(stmt.range());
let mut tree = match_module(module_text)?;
let mut tree = match_statement(module_text)?;
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
let Statement::Simple(body) = &mut tree else {
bail!("Expected Statement::Simple");
};
@@ -423,86 +420,6 @@ pub(crate) fn remove_argument(
}
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make the
/// symbol available in the current scope along with the bound name of the symbol.
///
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with the
/// name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
///
/// Attempts to reuse existing imports when possible.
pub(crate) fn get_or_import_symbol(
module: &str,
member: &str,
at: TextSize,
context: &Context,
importer: &Importer,
locator: &Locator,
) -> Result<(Edit, String)> {
if let Some((source, binding)) = context.resolve_qualified_import_name(module, member) {
// If the symbol is already available in the current scope, use it.
// The exception: the symbol source (i.e., the import statement) comes after the current
// location. For example, we could be generating an edit within a function, and the import
// could be defined in the module scope, but after the function definition. In this case,
// it's unclear whether we can use the symbol (the function could be called between the
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the top of the file, since it could
// be shadowed between the import and the current location.
if source.start() > at {
bail!("Unable to use existing symbol `{binding}` due to late-import");
}
// We also add a no-op edit to force conflicts with any other fixes that might try to
// remove the import. Consider:
//
// ```py
// import sys
//
// quit()
// ```
//
// Assume you omit this no-op edit. If you run Ruff with `unused-imports` and
// `sys-exit-alias` over this snippet, it will generate two fixes: (1) remove the unused
// `sys` import; and (2) replace `quit()` with `sys.exit()`, under the assumption that `sys`
// is already imported and available.
//
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
let import_edit =
Edit::range_replacement(locator.slice(source.range()).to_string(), source.range());
Ok((import_edit, binding))
} else {
if let Some(stmt) = importer.find_import_from(module, at) {
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
// bound name.
if context
.find_binding(member)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = importer.add_member(stmt, member)?;
Ok((import_edit, member.to_string()))
} else {
bail!("Unable to insert `{member}` into scope due to name conflict")
}
} else {
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
// return `"functools.cache"` as the bound name.
if context
.find_binding(module)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit =
importer.add_import(&AnyImport::Import(Import::module(module)), at);
Ok((import_edit, format!("{module}.{member}")))
} else {
bail!("Unable to insert `{module}` into scope due to name conflict")
}
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -1,7 +1,7 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::Expr;
use ruff_python_semantic::context::Snapshot;
use ruff_python_semantic::model::Snapshot;
/// A collection of AST nodes that are deferred for later analysis.
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ use ruff_python_stdlib::path::is_python_stub_file;
use crate::directives::IsortDirectives;
use crate::registry::Rule;
use crate::rules::isort;
use crate::rules::isort::track::{Block, ImportTracker};
use crate::rules::isort::block::{Block, BlockBuilder};
use crate::settings::Settings;
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
@@ -86,9 +86,9 @@ pub(crate) fn check_imports(
) -> (Vec<Diagnostic>, Option<ImportMap>) {
let is_stub = is_python_stub_file(path);
// Extract all imports from the AST.
// Extract all import blocks from the AST.
let tracker = {
let mut tracker = ImportTracker::new(locator, directives, is_stub);
let mut tracker = BlockBuilder::new(locator, directives, is_stub);
tracker.visit_body(python_ast);
tracker
};
@@ -109,7 +109,7 @@ pub(crate) fn check_imports(
}
if settings.rules.enabled(Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports(
&blocks, python_ast, locator, stylist, settings, is_stub,
python_ast, locator, stylist, settings, is_stub,
));
}

View File

@@ -183,6 +183,7 @@ mod tests {
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use crate::line_width::LineLength;
use crate::registry::Rule;
use crate::settings::Settings;
@@ -196,7 +197,7 @@ mod tests {
let indexer = Indexer::from_tokens(&tokens, &locator);
let stylist = Stylist::from_tokens(&tokens, &locator);
let check_with_max_line_length = |line_length: usize| {
let check_with_max_line_length = |line_length: LineLength| {
check_physical_lines(
Path::new("foo.py"),
&locator,
@@ -209,7 +210,8 @@ mod tests {
},
)
};
assert_eq!(check_with_max_line_length(8), vec![]);
assert_eq!(check_with_max_line_length(8), vec![]);
let line_length = LineLength::from(8);
assert_eq!(check_with_max_line_length(line_length), vec![]);
assert_eq!(check_with_max_line_length(line_length), vec![]);
}
}

View File

@@ -12,10 +12,11 @@ use crate::rules::{
};
use crate::settings::Settings;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::source_code::{Indexer, Locator};
pub(crate) fn check_tokens(
locator: &Locator,
indexer: &Indexer,
tokens: &[LexResult],
settings: &Settings,
is_stub: bool,
@@ -100,15 +101,9 @@ pub(crate) fn check_tokens(
// ERA001
if enforce_commented_out_code {
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, *range, settings)
{
diagnostics.push(diagnostic);
}
}
}
diagnostics.extend(eradicate::rules::commented_out_code(
indexer, locator, settings,
));
}
// W605
@@ -185,13 +180,13 @@ pub(crate) fn check_tokens(
// PYI033
if enforce_type_comment_in_stub && is_stub {
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(tokens));
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(indexer, locator));
}
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
if enforce_todos {
diagnostics.extend(
flake8_todos::rules::todos(tokens, settings)
flake8_todos::rules::todos(indexer, locator, settings)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);

View File

@@ -185,6 +185,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R5501") => (RuleGroup::Unspecified, Rule::CollapsibleElseIf),
(Pylint, "W0120") => (RuleGroup::Unspecified, Rule::UselessElseOnLoop),
(Pylint, "W0129") => (RuleGroup::Unspecified, Rule::AssertOnStringLiteral),
(Pylint, "W0131") => (RuleGroup::Unspecified, Rule::NamedExprWithoutContext),
(Pylint, "W0406") => (RuleGroup::Unspecified, Rule::ImportSelf),
(Pylint, "W0602") => (RuleGroup::Unspecified, Rule::GlobalVariableNotAssigned),
(Pylint, "W0603") => (RuleGroup::Unspecified, Rule::GlobalStatement),
@@ -192,6 +193,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W1508") => (RuleGroup::Unspecified, Rule::InvalidEnvvarDefault),
(Pylint, "W2901") => (RuleGroup::Unspecified, Rule::RedefinedLoopName),
(Pylint, "W3301") => (RuleGroup::Unspecified, Rule::NestedMinMax),
(Pylint, "W0130") => (RuleGroup::Unspecified, Rule::DuplicateValue),
// flake8-async
(Flake8Async, "100") => (RuleGroup::Unspecified, Rule::BlockingHttpCallInAsyncFunction),
@@ -507,6 +509,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "506") => (RuleGroup::Unspecified, Rule::UnsafeYAMLLoad),
(Flake8Bandit, "508") => (RuleGroup::Unspecified, Rule::SnmpInsecureVersion),
(Flake8Bandit, "509") => (RuleGroup::Unspecified, Rule::SnmpWeakCryptography),
(Flake8Bandit, "601") => (RuleGroup::Unspecified, Rule::ParamikoCall),
(Flake8Bandit, "602") => (RuleGroup::Unspecified, Rule::SubprocessPopenWithShellEqualsTrue),
(Flake8Bandit, "603") => (RuleGroup::Unspecified, Rule::SubprocessWithoutShellEqualsTrue),
(Flake8Bandit, "604") => (RuleGroup::Unspecified, Rule::CallWithShellEqualsTrue),
@@ -580,6 +583,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "010") => (RuleGroup::Unspecified, Rule::NonEmptyStubBody),
(Flake8Pyi, "011") => (RuleGroup::Unspecified, Rule::TypedArgumentDefaultInStub),
(Flake8Pyi, "012") => (RuleGroup::Unspecified, Rule::PassInClassBody),
(Flake8Pyi, "013") => (RuleGroup::Unspecified, Rule::EllipsisInNonEmptyClassBody),
(Flake8Pyi, "014") => (RuleGroup::Unspecified, Rule::ArgumentDefaultInStub),
(Flake8Pyi, "015") => (RuleGroup::Unspecified, Rule::AssignmentDefaultInStub),
(Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember),
@@ -733,13 +737,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flynt, "002") => (RuleGroup::Unspecified, Rule::StaticJoinToFString),
// flake8-todos
(Flake8Todo, "001") => (RuleGroup::Unspecified, Rule::InvalidTodoTag),
(Flake8Todo, "002") => (RuleGroup::Unspecified, Rule::MissingTodoAuthor),
(Flake8Todo, "003") => (RuleGroup::Unspecified, Rule::MissingTodoLink),
(Flake8Todo, "004") => (RuleGroup::Unspecified, Rule::MissingTodoColon),
(Flake8Todo, "005") => (RuleGroup::Unspecified, Rule::MissingTodoDescription),
(Flake8Todo, "006") => (RuleGroup::Unspecified, Rule::InvalidTodoCapitalization),
(Flake8Todo, "007") => (RuleGroup::Unspecified, Rule::MissingSpaceAfterTodoColon),
(Flake8Todos, "001") => (RuleGroup::Unspecified, Rule::InvalidTodoTag),
(Flake8Todos, "002") => (RuleGroup::Unspecified, Rule::MissingTodoAuthor),
(Flake8Todos, "003") => (RuleGroup::Unspecified, Rule::MissingTodoLink),
(Flake8Todos, "004") => (RuleGroup::Unspecified, Rule::MissingTodoColon),
(Flake8Todos, "005") => (RuleGroup::Unspecified, Rule::MissingTodoDescription),
(Flake8Todos, "006") => (RuleGroup::Unspecified, Rule::InvalidTodoCapitalization),
(Flake8Todos, "007") => (RuleGroup::Unspecified, Rule::MissingSpaceAfterTodoColon),
_ => return None,
})

View File

@@ -1,7 +1,9 @@
use anyhow::{bail, Result};
use libcst_native::{
Attribute, Call, Comparison, Dict, Expr, Expression, Import, ImportAlias, ImportFrom,
ImportNames, Module, SimpleString, SmallStatement, Statement,
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name,
SimpleString, SmallStatement, Statement, Suite, Tuple, With,
};
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
@@ -18,20 +20,15 @@ pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
}
}
pub(crate) fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
}
} else {
bail!("Expected Statement::Simple")
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
Err(_) => bail!("Failed to extract statement from source"),
}
}
pub(crate) fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
pub(crate) fn match_import<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut Import<'b>> {
if let Statement::Simple(expr) = statement {
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
@@ -43,9 +40,9 @@ pub(crate) fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut
}
pub(crate) fn match_import_from<'a, 'b>(
module: &'a mut Module<'b>,
statement: &'a mut Statement<'b>,
) -> Result<&'a mut ImportFrom<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Statement::Simple(expr) = statement {
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
@@ -66,7 +63,17 @@ pub(crate) fn match_aliases<'a, 'b>(
}
}
pub(crate) fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
pub(crate) fn match_call<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Call<'b>> {
if let Expression::Call(call) = expression {
Ok(call)
} else {
bail!("Expected Expression::Call")
}
}
pub(crate) fn match_call_mut<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = expression {
Ok(call)
} else {
@@ -111,3 +118,123 @@ pub(crate) fn match_simple_string<'a, 'b>(
bail!("Expected Expression::SimpleString")
}
}
pub(crate) fn match_formatted_string<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut FormattedString<'b>> {
if let Expression::FormattedString(formatted_string) = expression {
Ok(formatted_string)
} else {
bail!("Expected Expression::FormattedString")
}
}
pub(crate) fn match_formatted_string_expression<'a, 'b>(
formatted_string_content: &'a mut FormattedStringContent<'b>,
) -> Result<&'a mut FormattedStringExpression<'b>> {
if let FormattedStringContent::Expression(formatted_string_expression) =
formatted_string_content
{
Ok(formatted_string_expression)
} else {
bail!("Expected FormattedStringContent::Expression")
}
}
pub(crate) fn match_name<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Name<'b>> {
if let Expression::Name(name) = expression {
Ok(name)
} else {
bail!("Expected Expression::Name")
}
}
pub(crate) fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> {
if let Some(arg) = call.args.first() {
Ok(arg)
} else {
bail!("Expected Arg")
}
}
pub(crate) fn match_generator_exp<'a, 'b>(
expression: &'a Expression<'b>,
) -> Result<&'a GeneratorExp<'b>> {
if let Expression::GeneratorExp(generator_exp) = expression {
Ok(generator_exp)
} else {
bail!("Expected Expression::GeneratorExp")
}
}
pub(crate) fn match_tuple<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Tuple<'b>> {
if let Expression::Tuple(tuple) = expression {
Ok(tuple)
} else {
bail!("Expected Expression::Tuple")
}
}
pub(crate) fn match_list_comp<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a ListComp<'b>> {
if let Expression::ListComp(list_comp) = expression {
Ok(list_comp)
} else {
bail!("Expected Expression::ListComp")
}
}
pub(crate) fn match_lambda<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Lambda<'b>> {
if let Expression::Lambda(lambda) = expression {
Ok(lambda)
} else {
bail!("Expected Expression::Lambda")
}
}
pub(crate) fn match_function_def<'a, 'b>(
statement: &'a mut Statement<'b>,
) -> Result<&'a mut FunctionDef<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::FunctionDef(function_def) = compound {
Ok(function_def)
} else {
bail!("Expected CompoundStatement::FunctionDef")
}
} else {
bail!("Expected Statement::Compound")
}
}
pub(crate) fn match_indented_block<'a, 'b>(
suite: &'a mut Suite<'b>,
) -> Result<&'a mut IndentedBlock<'b>> {
if let Suite::IndentedBlock(indented_block) = suite {
Ok(indented_block)
} else {
bail!("Expected Suite::IndentedBlock")
}
}
pub(crate) fn match_with<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut With<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::With(with) = compound {
Ok(with)
} else {
bail!("Expected CompoundStatement::With")
}
} else {
bail!("Expected Statement::Compound")
}
}
pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut If<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::If(if_) = compound {
Ok(if_)
} else {
bail!("Expected CompoundStatement::If")
}
} else {
bail!("Expected Statement::Compound")
}
}

View File

@@ -83,11 +83,7 @@ pub fn extract_directives(
}
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(
lxr: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) -> NoqaMapping {
fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer) -> NoqaMapping {
let mut string_mappings = Vec::new();
for (tok, range) in lxr.iter().flatten() {
@@ -166,7 +162,7 @@ pub fn extract_noqa_line_for(
}
/// Extract a set of ranges over which to disable isort.
pub fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
let mut exclusions: Vec<TextRange> = Vec::default();
let mut splits: Vec<TextSize> = Vec::default();
let mut off: Option<TextSize> = None;

View File

@@ -57,11 +57,6 @@ impl<'a> DocstringBody<'a> {
self.range().start()
}
#[inline]
pub(crate) fn end(self) -> TextSize {
self.range().end()
}
pub(crate) fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}

View File

@@ -1,15 +1,17 @@
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
use ruff_text_size::{TextLen, TextRange, TextSize};
use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
use ruff_python_ast::whitespace;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
use ruff_python_ast::whitespace;
#[derive(EnumIter, PartialEq, Eq, Debug, Clone, Copy)]
pub enum SectionKind {
pub(crate) enum SectionKind {
Args,
Arguments,
Attention,
@@ -48,7 +50,7 @@ pub enum SectionKind {
}
impl SectionKind {
pub fn from_str(s: &str) -> Option<Self> {
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"args" => Some(Self::Args),
"arguments" => Some(Self::Arguments),
@@ -89,7 +91,7 @@ impl SectionKind {
}
}
pub fn as_str(self) -> &'static str {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Args => "Args",
Self::Arguments => "Arguments",
@@ -217,7 +219,7 @@ impl Debug for SectionContexts<'_> {
}
}
pub struct SectionContextsIter<'a> {
pub(crate) struct SectionContextsIter<'a> {
docstring_body: DocstringBody<'a>,
inner: std::slice::Iter<'a, SectionContextData>,
}
@@ -266,28 +268,24 @@ struct SectionContextData {
summary_full_end: TextSize,
}
pub struct SectionContext<'a> {
pub(crate) struct SectionContext<'a> {
data: &'a SectionContextData,
docstring_body: DocstringBody<'a>,
}
impl<'a> SectionContext<'a> {
pub fn is_last(&self) -> bool {
self.range().end() == self.docstring_body.end()
}
/// The `kind` of the section, e.g. [`SectionKind::Args`] or [`SectionKind::Returns`].
pub const fn kind(&self) -> SectionKind {
pub(crate) const fn kind(&self) -> SectionKind {
self.data.kind
}
/// The name of the section as it appears in the docstring, e.g. "Args" or "Returns".
pub fn section_name(&self) -> &'a str {
pub(crate) fn section_name(&self) -> &'a str {
&self.docstring_body.as_str()[self.data.name_range]
}
/// Returns the rest of the summary line after the section name.
pub fn summary_after_section_name(&self) -> &'a str {
pub(crate) fn summary_after_section_name(&self) -> &'a str {
&self.summary_line()[usize::from(self.data.name_range.end() - self.data.range.start())..]
}
@@ -296,17 +294,12 @@ impl<'a> SectionContext<'a> {
}
/// The absolute range of the section name
pub fn section_name_range(&self) -> TextRange {
pub(crate) fn section_name_range(&self) -> TextRange {
self.data.name_range + self.offset()
}
/// Summary range relative to the start of the document. Includes the trailing newline.
pub fn summary_full_range(&self) -> TextRange {
self.summary_full_range_relative() + self.offset()
}
/// The absolute range of the summary line, excluding any trailing newline character.
pub fn summary_range(&self) -> TextRange {
pub(crate) fn summary_range(&self) -> TextRange {
TextRange::at(self.range().start(), self.summary_line().text_len())
}
@@ -321,12 +314,12 @@ impl<'a> SectionContext<'a> {
}
/// The absolute range of the full-section.
pub fn range(&self) -> TextRange {
pub(crate) fn range(&self) -> TextRange {
self.range_relative() + self.offset()
}
/// Summary line without the trailing newline characters
pub fn summary_line(&self) -> &'a str {
pub(crate) fn summary_line(&self) -> &'a str {
let full_summary = &self.docstring_body.as_str()[self.summary_full_range_relative()];
let mut bytes = full_summary.bytes().rev();
@@ -347,14 +340,14 @@ impl<'a> SectionContext<'a> {
}
/// Returns the text of the last line of the previous section or an empty string if it is the first section.
pub fn previous_line(&self) -> Option<&'a str> {
pub(crate) fn previous_line(&self) -> Option<&'a str> {
let previous =
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
previous.universal_newlines().last().map(|l| l.as_str())
}
/// Returns the lines belonging to this section after the summary line.
pub fn following_lines(&self) -> UniversalNewlineIterator<'a> {
pub(crate) fn following_lines(&self) -> UniversalNewlineIterator<'a> {
let lines = self.following_lines_str();
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
}
@@ -369,7 +362,7 @@ impl<'a> SectionContext<'a> {
}
/// Returns the absolute range of the following lines.
pub fn following_range(&self) -> TextRange {
pub(crate) fn following_range(&self) -> TextRange {
self.following_range_relative() + self.offset()
}
}

View File

@@ -3,16 +3,14 @@ use std::collections::{HashMap, HashSet};
use anyhow::Result;
use itertools::Itertools;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
use crate::line_width::LineLength;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
use crate::rules::flake8_quotes::settings::Quote;
use crate::rules::flake8_tidy_imports::relative_imports::Strictness;
use crate::rules::flake8_tidy_imports::settings::Strictness;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{
flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style,
@@ -23,6 +21,10 @@ use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use crate::warn_user;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
const DEFAULT_SELECTORS: &[RuleSelector] = &[
RuleSelector::Linter(Linter::Pyflakes),
RuleSelector::Linter(Linter::Pycodestyle),
@@ -119,7 +121,9 @@ pub fn convert(
options.builtins = Some(parser::parse_strings(value.as_ref()));
}
"max-line-length" | "max_line_length" => match value.parse::<usize>() {
Ok(line_length) => options.line_length = Some(line_length),
Ok(line_length) => {
options.line_length = Some(LineLength::from(line_length));
}
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
@@ -402,7 +406,7 @@ pub fn convert(
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = &external_config.black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
options.line_length = Some(LineLength::from(*line_length));
}
if let Some(target_version) = &black.target_version {
@@ -456,11 +460,10 @@ mod tests {
use pep440_rs::VersionSpecifiers;
use pretty_assertions::assert_eq;
use super::super::plugin::Plugin;
use super::convert;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::pep621::Project;
use crate::flake8_to_ruff::ExternalConfig;
use crate::line_width::LineLength;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
@@ -469,6 +472,9 @@ mod tests {
use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use super::super::plugin::Plugin;
use super::convert;
fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
Options {
ignore: Some(vec![]),
@@ -508,7 +514,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
line_length: Some(LineLength::from(100)),
..default_options([])
});
assert_eq!(actual, expected);
@@ -527,7 +533,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
line_length: Some(LineLength::from(100)),
..default_options([])
});
assert_eq!(actual, expected);

View File

@@ -1,3 +1,8 @@
pub use converter::convert;
pub use external_config::ExternalConfig;
pub use plugin::Plugin;
pub use pyproject::parse;
mod black;
mod converter;
mod external_config;
@@ -6,8 +11,3 @@ mod parser;
pub mod pep621;
mod plugin;
mod pyproject;
pub use converter::convert;
pub use external_config::ExternalConfig;
pub use plugin::Plugin;
pub use pyproject::parse;

View File

@@ -195,12 +195,13 @@ pub(crate) fn collect_per_file_ignores(
mod tests {
use anyhow::Result;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use crate::codes;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
#[test]
fn it_parses_prefix_codes() {
let actual = parse_prefix_codes("");

View File

@@ -1,8 +1,8 @@
use ruff_diagnostics::Edit;
use ruff_text_size::TextSize;
use rustpython_parser::ast::{Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::source_code::{Locator, Stylist};

View File

@@ -1,15 +1,16 @@
//! Add and modify import statements to make module members available during fix execution.
use anyhow::Result;
use anyhow::{bail, Result};
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
use ruff_text_size::TextSize;
use rustpython_parser::ast::{self, Ranged, Stmt, Suite};
use ruff_diagnostics::Edit;
use ruff_python_ast::imports::AnyImport;
use ruff_python_ast::imports::{AnyImport, Import};
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_semantic::model::SemanticModel;
use crate::cst::matchers::{match_aliases, match_import_from, match_module};
use crate::cst::matchers::{match_aliases, match_import_from, match_statement};
use crate::importer::insertion::Insertion;
mod insertion;
@@ -40,14 +41,6 @@ impl<'a> Importer<'a> {
self.ordered_imports.push(import);
}
/// Return the import statement that precedes the given position, if any.
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
self.ordered_imports
.partition_point(|stmt| stmt.start() < at)
.checked_sub(1)
.map(|idx| self.ordered_imports[idx])
}
/// Add an import statement to import the given module.
///
/// If there are no existing imports, the new import will be added at the top
@@ -66,9 +59,123 @@ impl<'a> Importer<'a> {
}
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
/// the symbol available in the current scope along with the bound name of the symbol.
///
/// Attempts to reuse existing imports when possible.
pub(crate) fn get_or_import_symbol(
&self,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<(Edit, String)> {
self.get_symbol(module, member, at, semantic_model)?
.map_or_else(
|| self.import_symbol(module, member, at, semantic_model),
Ok,
)
}
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
fn get_symbol(
&self,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<Option<(Edit, String)>> {
// If the symbol is already available in the current scope, use it.
let Some((source, binding)) = semantic_model.resolve_qualified_import_name(module, member) else {
return Ok(None);
};
// The exception: the symbol source (i.e., the import statement) comes after the current
// location. For example, we could be generating an edit within a function, and the import
// could be defined in the module scope, but after the function definition. In this case,
// it's unclear whether we can use the symbol (the function could be called between the
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the top of the file, since it could
// be shadowed between the import and the current location.
if source.start() > at {
bail!("Unable to use existing symbol `{binding}` due to late-import");
}
// We also add a no-op edit to force conflicts with any other fixes that might try to
// remove the import. Consider:
//
// ```py
// import sys
//
// quit()
// ```
//
// Assume you omit this no-op edit. If you run Ruff with `unused-imports` and
// `sys-exit-alias` over this snippet, it will generate two fixes: (1) remove the unused
// `sys` import; and (2) replace `quit()` with `sys.exit()`, under the assumption that `sys`
// is already imported and available.
//
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
let import_edit = Edit::range_replacement(
self.locator.slice(source.range()).to_string(),
source.range(),
);
Ok(Some((import_edit, binding)))
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
/// the symbol available in the current scope along with the bound name of the symbol.
///
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with
/// the name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
fn import_symbol(
&self,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<(Edit, String)> {
if let Some(stmt) = self.find_import_from(module, at) {
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
// bound name.
if semantic_model
.find_binding(member)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = self.add_member(stmt, member)?;
Ok((import_edit, member.to_string()))
} else {
bail!("Unable to insert `{member}` into scope due to name conflict")
}
} else {
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
// return `"functools.cache"` as the bound name.
if semantic_model
.find_binding(module)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = self.add_import(&AnyImport::Import(Import::module(module)), at);
Ok((import_edit, format!("{module}.{member}")))
} else {
bail!("Unable to insert `{module}` into scope due to name conflict")
}
}
}
/// Return the import statement that precedes the given position, if any.
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
self.ordered_imports
.partition_point(|stmt| stmt.start() < at)
.checked_sub(1)
.map(|idx| self.ordered_imports[idx])
}
/// Return the top-level [`Stmt`] that imports the given module using `Stmt::ImportFrom`
/// preceding the given position, if any.
pub(crate) fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
let mut import_from = None;
for stmt in &self.ordered_imports {
if stmt.start() >= at {
@@ -91,9 +198,9 @@ impl<'a> Importer<'a> {
}
/// Add the given member to an existing `Stmt::ImportFrom` statement.
pub(crate) fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut tree = match_module(self.locator.slice(stmt.range()))?;
let import_from = match_import_from(&mut tree)?;
fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut statement = match_statement(self.locator.slice(stmt.range()))?;
let import_from = match_import_from(&mut statement)?;
let aliases = match_aliases(import_from)?;
aliases.push(ImportAlias {
name: NameOrAttribute::N(Box::new(Name {
@@ -109,7 +216,7 @@ impl<'a> Importer<'a> {
default_indent: self.stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
statement.codegen(&mut state);
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
}
}

View File

@@ -1,7 +1,7 @@
//! Utils for reading and writing jupyter notebooks
mod notebook;
mod schema;
pub use notebook::*;
pub use schema::*;
mod notebook;
mod schema;

View File

@@ -1,9 +1,9 @@
use ruff_text_size::TextRange;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::iter;
use std::path::Path;
use ruff_text_size::TextRange;
use serde::Serialize;
use serde_json::error::Category;

View File

@@ -21,6 +21,7 @@ pub mod fs;
mod importer;
pub mod jupyter;
mod lex;
pub mod line_width;
pub mod linter;
pub mod logging;
pub mod message;

View File

@@ -0,0 +1,165 @@
use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
/// The length of a line of text that is considered too long.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LineLength(usize);
impl Default for LineLength {
/// The default line length.
fn default() -> Self {
Self(88)
}
}
impl LineLength {
pub const fn get(&self) -> usize {
self.0
}
}
impl From<usize> for LineLength {
fn from(value: usize) -> Self {
Self(value)
}
}
/// A measure of the width of a line of text.
///
/// This is used to determine if a line is too long.
/// It should be compared to a [`LineLength`].
#[derive(Clone, Copy, Debug)]
pub struct LineWidth {
/// The width of the line.
width: usize,
/// The column of the line.
/// This is used to calculate the width of tabs.
column: usize,
/// The tab size to use when calculating the width of tabs.
tab_size: TabSize,
}
impl Default for LineWidth {
fn default() -> Self {
Self::new(TabSize::default())
}
}
impl PartialEq for LineWidth {
fn eq(&self, other: &Self) -> bool {
self.width == other.width
}
}
impl Eq for LineWidth {}
impl PartialOrd for LineWidth {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.width.partial_cmp(&other.width)
}
}
impl Ord for LineWidth {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.width.cmp(&other.width)
}
}
impl LineWidth {
pub fn get(&self) -> usize {
self.width
}
/// Creates a new `LineWidth` with the given tab size.
pub fn new(tab_size: TabSize) -> Self {
LineWidth {
width: 0,
column: 0,
tab_size,
}
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.into();
for c in chars {
match c {
'\t' => {
let tab_offset = tab_size - (self.column % tab_size);
self.width += tab_offset;
self.column += tab_offset;
}
'\n' | '\r' => {
self.width = 0;
self.column = 0;
}
_ => {
self.width += c.width().unwrap_or(0);
self.column += 1;
}
}
}
self
}
/// Adds the given text to the line width.
#[must_use]
pub fn add_str(self, text: &str) -> Self {
self.update(text.chars())
}
/// Adds the given character to the line width.
#[must_use]
pub fn add_char(self, c: char) -> Self {
self.update(std::iter::once(c))
}
/// Adds the given width to the line width.
/// Also adds the given width to the column.
/// It is generally better to use [`LineWidth::add_str`] or [`LineWidth::add_char`].
/// The width and column should be the same for the corresponding text.
/// Currently, this is only used to add spaces.
#[must_use]
pub fn add_width(mut self, width: usize) -> Self {
self.width += width;
self.column += width;
self
}
}
impl PartialEq<LineLength> for LineWidth {
fn eq(&self, other: &LineLength) -> bool {
self.width == other.0
}
}
impl PartialOrd<LineLength> for LineWidth {
fn partial_cmp(&self, other: &LineLength) -> Option<std::cmp::Ordering> {
self.width.partial_cmp(&other.0)
}
}
/// The size of a tab.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabSize(pub u8);
impl Default for TabSize {
fn default() -> Self {
Self(4)
}
}
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

View File

@@ -98,7 +98,7 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_tokens())
{
let is_stub = is_python_stub_file(path);
diagnostics.extend(check_tokens(locator, &tokens, settings, is_stub));
diagnostics.extend(check_tokens(locator, indexer, &tokens, settings, is_stub));
}
// Run the filesystem-based rules.

View File

@@ -2,15 +2,17 @@ use std::fmt::{Display, Formatter, Write};
use std::path::Path;
use std::sync::Mutex;
use crate::fs;
use anyhow::Result;
use colored::Colorize;
use fern;
use log::Level;
use once_cell::sync::Lazy;
use ruff_python_ast::source_code::SourceCode;
use rustpython_parser::{ParseError, ParseErrorType};
use ruff_python_ast::source_code::SourceCode;
use crate::fs;
pub(crate) static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
/// Warn a user once, with uniqueness determined by the given ID.

View File

@@ -1,7 +1,9 @@
use std::io::Write;
use ruff_python_ast::source_code::SourceLocation;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
/// 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)
@@ -42,9 +44,10 @@ impl Emitter for AzureEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::AzureEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,12 +1,15 @@
use crate::message::Message;
use colored::{Color, ColoredString, Colorize, Styles};
use ruff_diagnostics::{Applicability, Fix};
use ruff_python_ast::source_code::{OneIndexed, SourceFile};
use ruff_text_size::{TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use std::fmt::{Display, Formatter};
use std::num::NonZeroUsize;
use colored::{Color, ColoredString, Colorize, Styles};
use ruff_text_size::{TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use ruff_diagnostics::{Applicability, Fix};
use ruff_python_ast::source_code::{OneIndexed, SourceFile};
use crate::message::Message;
/// Renders a diff that shows the code fixes.
///
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try

View File

@@ -1,8 +1,10 @@
use std::io::Write;
use ruff_python_ast::source_code::SourceLocation;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
/// 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)
@@ -57,9 +59,10 @@ impl Emitter for GithubEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GithubEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,14 +1,17 @@
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Write;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_python_ast::source_code::SourceLocation;
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
pub struct GitlabEmitter {
@@ -122,9 +125,10 @@ fn fingerprint(
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GitlabEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,3 +1,11 @@
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::num::NonZeroUsize;
use colored::Colorize;
use ruff_python_ast::source_code::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::JupyterIndex;
use crate::message::diff::calculate_print_width;
@@ -5,11 +13,6 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use colored::Colorize;
use ruff_python_ast::source_code::OneIndexed;
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::num::NonZeroUsize;
#[derive(Default)]
pub struct GroupedEmitter {
@@ -175,9 +178,10 @@ impl std::fmt::Write for PadAdapter<'_> {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GroupedEmitter;
use insta::assert_snapshot;
#[test]
fn default() {

View File

@@ -1,11 +1,14 @@
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_diagnostics::Edit;
use ruff_python_ast::source_code::SourceCode;
use std::io::Write;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use std::io::Write;
use ruff_diagnostics::Edit;
use ruff_python_ast::source_code::SourceCode;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
#[derive(Default)]
pub struct JsonEmitter;
@@ -94,9 +97,10 @@ impl Serialize for ExpandedEdits<'_> {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::JsonEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,11 +1,14 @@
use std::io::Write;
use std::path::Path;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
use ruff_python_ast::source_code::SourceLocation;
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::registry::AsRule;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
use std::path::Path;
#[derive(Default)]
pub struct JunitEmitter;
@@ -19,49 +22,60 @@ impl Emitter for JunitEmitter {
) -> anyhow::Result<()> {
let mut report = Report::new("ruff");
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(filename);
if messages.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert("package".to_string(), "org.ruff".to_string());
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra
.insert("line".to_string(), location.row.to_string());
case.extra
.insert("column".to_string(), location.column.to_string());
test_suite.add_test_case(case);
}
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
.insert("package".to_string(), "org.ruff".to_string());
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra
.insert("line".to_string(), location.row.to_string());
case.extra
.insert("column".to_string(), location.column.to_string());
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
report.serialize(writer)?;
@@ -72,9 +86,10 @@ impl Emitter for JunitEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::JunitEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,3 +1,23 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use crate::jupyter::JupyterIndex;
pub use azure::AzureEmitter;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
pub use grouped::GroupedEmitter;
pub use json::JsonEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_python_ast::source_code::{SourceFile, SourceLocation};
pub use text::TextEmitter;
mod azure;
mod diff;
mod github;
@@ -8,27 +28,6 @@ mod junit;
mod pylint;
mod text;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
pub use azure::AzureEmitter;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
pub use grouped::GroupedEmitter;
pub use json::JsonEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
pub use text::TextEmitter;
use crate::jupyter::JupyterIndex;
use crate::registry::AsRule;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_python_ast::source_code::{SourceFile, SourceLocation};
#[derive(Debug, PartialEq, Eq)]
pub struct Message {
pub kind: DiagnosticKind,
@@ -76,11 +75,7 @@ impl Message {
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(self.filename(), self.start(), self.kind.rule()).cmp(&(
other.filename(),
other.start(),
other.kind.rule(),
))
(&self.file, self.start()).cmp(&(&other.file, other.start()))
}
}
@@ -152,13 +147,15 @@ impl<'a> EmitterContext<'a> {
#[cfg(test)]
mod tests {
use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::SourceFileBuilder;
use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
pub(super) fn create_messages() -> Vec<Message> {
let fib = r#"import os

View File

@@ -1,8 +1,10 @@
use std::io::Write;
use ruff_python_ast::source_code::OneIndexed;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::OneIndexed;
use std::io::Write;
/// Generate violations in Pylint format.
/// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter)
@@ -40,9 +42,10 @@ impl Emitter for PylintEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::PylintEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,22 +1,29 @@
use crate::fs::relativize_path;
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::io::Write;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use ruff_text_size::{TextRange, TextSize};
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use crate::fs::relativize_path;
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
bitflags! {
#[derive(Default)]
struct EmitterFlags: u8 {
/// Whether to show the fix status of a diagnostic.
const SHOW_FIX_STATUS = 0b0000_0001;
const SHOW_FIX = 0b0000_0010;
/// Whether to show the diff of a fix, for diagnostics that have a fix.
const SHOW_FIX_DIFF = 0b0000_0010;
/// Whether to show the source code of a diagnostic.
const SHOW_SOURCE = 0b0000_0100;
}
}
@@ -35,8 +42,8 @@ impl TextEmitter {
}
#[must_use]
pub fn with_show_fix(mut self, show_fix: bool) -> Self {
self.flags.set(EmitterFlags::SHOW_FIX, show_fix);
pub fn with_show_fix_diff(mut self, show_fix_diff: bool) -> Self {
self.flags.set(EmitterFlags::SHOW_FIX_DIFF, show_fix_diff);
self
}
@@ -101,7 +108,7 @@ impl Emitter for TextEmitter {
writeln!(writer, "{}", MessageCodeFrame { message })?;
}
if self.flags.contains(EmitterFlags::SHOW_FIX) {
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
if let Some(diff) = Diff::from_message(message) {
writeln!(writer, "{diff}")?;
}
@@ -234,39 +241,35 @@ impl Display for MessageCodeFrame<'_> {
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: u32 = 4; // TODO(jonathan): use `pycodestyle.tab-size`
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut column = 0;
let mut line_width = LineWidth::new(TAB_SIZE);
for (index, c) in source.chars().enumerate() {
match c {
'\t' => {
let tab_width = TAB_SIZE - column % TAB_SIZE;
column += tab_width;
for (index, c) in source.char_indices() {
let old_width = line_width.get();
line_width = line_width.add_char(c);
if index < usize::from(annotation_range.start()) {
range += TextSize::new(tab_width - 1);
} else if index < usize::from(annotation_range.end()) {
range = range.add_end(TextSize::new(tab_width - 1));
}
if matches!(c, '\t') {
// SAFETY: The difference is a value in the range [1..TAB_SIZE] which is guaranteed to be less than `u32`.
#[allow(clippy::cast_possible_truncation)]
let tab_width = (line_width.get() - old_width) as u32;
result.push_str(&source[last_end..index]);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
if index < usize::from(annotation_range.start()) {
range += TextSize::new(tab_width - 1);
} else if index < usize::from(annotation_range.end()) {
range = range.add_end(TextSize::new(tab_width - 1));
}
'\n' | '\r' => {
column = 0;
}
_ => {
column += 1;
result.push_str(&source[last_end..index]);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
}
}
@@ -292,9 +295,10 @@ struct SourceCode<'a> {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::TextEmitter;
use insta::assert_snapshot;
#[test]
fn default() {

View File

@@ -24,7 +24,6 @@ static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
)
.unwrap()
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
#[derive(Debug)]
pub(crate) enum Directive<'a> {
@@ -46,12 +45,12 @@ pub(crate) fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator)
caps.name("trailing_spaces"),
) {
(Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.as_str().trim())
let codes = codes
.as_str()
.split(|c: char| c.is_whitespace() || c == ',')
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
.collect_vec();
let start = range.start() + TextSize::try_from(noqa.start()).unwrap();
if codes.is_empty() {
#[allow(deprecated)]
@@ -105,11 +104,11 @@ fn parse_file_exemption(line: &str) -> ParsedExemption {
if remainder.is_empty() {
return ParsedExemption::All;
} else if let Some(codes) = remainder.strip_prefix(':') {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.trim())
let codes = codes
.split(|c: char| c.is_whitespace() || c == ',')
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
.collect_vec();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}

View File

@@ -1,15 +1,15 @@
//! Registry of all [`Rule`] implementations.
mod rule_set;
use strum_macros::{AsRefStr, EnumIter};
use ruff_diagnostics::Violation;
use ruff_macros::RuleNamespace;
pub use rule_set::{RuleSet, RuleSetIterator};
use crate::codes::{self, RuleCodePrefix};
use crate::rules;
pub use rule_set::{RuleSet, RuleSetIterator};
mod rule_set;
ruff_macros::register_rules!(
// pycodestyle errors
@@ -159,7 +159,9 @@ ruff_macros::register_rules!(
rules::pylint::rules::LoggingTooManyArgs,
rules::pylint::rules::UnexpectedSpecialMethodSignature,
rules::pylint::rules::NestedMinMax,
rules::pylint::rules::DuplicateValue,
rules::pylint::rules::DuplicateBases,
rules::pylint::rules::NamedExprWithoutContext,
// flake8-async
rules::flake8_async::rules::BlockingHttpCallInAsyncFunction,
rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction,
@@ -228,8 +230,8 @@ ruff_macros::register_rules!(
// mccabe
rules::mccabe::rules::ComplexStructure,
// flake8-tidy-imports
rules::flake8_tidy_imports::banned_api::BannedApi,
rules::flake8_tidy_imports::relative_imports::RelativeImports,
rules::flake8_tidy_imports::rules::BannedApi,
rules::flake8_tidy_imports::rules::RelativeImports,
// flake8-return
rules::flake8_return::rules::UnnecessaryReturnNone,
rules::flake8_return::rules::ImplicitReturnValue,
@@ -422,6 +424,7 @@ ruff_macros::register_rules!(
rules::flake8_bandit::rules::HardcodedTempFile,
rules::flake8_bandit::rules::HashlibInsecureHashFunction,
rules::flake8_bandit::rules::Jinja2AutoescapeFalse,
rules::flake8_bandit::rules::ParamikoCall,
rules::flake8_bandit::rules::LoggingConfigInsecureListen,
rules::flake8_bandit::rules::RequestWithNoCertValidation,
rules::flake8_bandit::rules::RequestWithoutTimeout,
@@ -510,6 +513,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::BadVersionInfoComparison,
rules::flake8_pyi::rules::DocstringInStub,
rules::flake8_pyi::rules::DuplicateUnionMember,
rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody,
rules::flake8_pyi::rules::NonEmptyStubBody,
rules::flake8_pyi::rules::PassInClassBody,
rules::flake8_pyi::rules::PassStatementStubBody,
@@ -808,7 +812,7 @@ pub enum Linter {
Flake8UsePathlib,
/// [flake8-todos](https://github.com/orsinium-labs/flake8-todos/)
#[prefix = "TD"]
Flake8Todo,
Flake8Todos,
/// [eradicate](https://pypi.org/project/eradicate/)
#[prefix = "ERA"]
Eradicate,
@@ -999,6 +1003,7 @@ pub const INCOMPATIBLE_CODES: &[(Rule, Rule, &str); 2] = &[
#[cfg(test)]
mod tests {
use std::mem::size_of;
use strum::IntoEnumIterator;
use super::{Linter, Rule, RuleNamespace};

View File

@@ -5,7 +5,7 @@ use rustpython_parser as parser;
static ALLOWLIST_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"^(?i)(?:pylint|pyright|noqa|nosec|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
r"^(?i)(?:pylint|pyright|noqa|nosec|region|endregion|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
).unwrap()
});
static BRACKET_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[()\[\]{}\s]+$").unwrap());
@@ -224,6 +224,11 @@ mod tests {
assert!(!comment_contains_code("# noqa: A123", &[]));
assert!(!comment_contains_code("# noqa:A123", &[]));
assert!(!comment_contains_code("# nosec", &[]));
assert!(!comment_contains_code("# region", &[]));
assert!(!comment_contains_code("# endregion", &[]));
assert!(!comment_contains_code("# region.name", &[]));
assert!(!comment_contains_code("# region name", &[]));
assert!(!comment_contains_code("# region: name", &[]));
assert!(!comment_contains_code("# fmt: on", &[]));
assert!(!comment_contains_code("# fmt: off", &[]));
assert!(!comment_contains_code("# fmt:on", &[]));

View File

@@ -7,7 +7,6 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::registry::Rule;

View File

@@ -1,13 +1,11 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::source_code::{Indexer, Locator};
use crate::registry::Rule;
use crate::settings::Settings;
use super::detection::comment_contains_code;
use super::super::detection::comment_contains_code;
/// ## What it does
/// Checks for commented-out Python code.
@@ -47,24 +45,28 @@ fn is_standalone_comment(line: &str) -> bool {
/// ERA001
pub(crate) fn commented_out_code(
indexer: &Indexer,
locator: &Locator,
range: TextRange,
settings: &Settings,
) -> Option<Diagnostic> {
let line = locator.full_lines(range);
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
for range in indexer.comment_ranges() {
let line = locator.full_lines(*range);
if settings.rules.should_fix(Rule::CommentedOutCode) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
locator.full_lines_range(range),
)));
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
if settings.rules.should_fix(Rule::CommentedOutCode) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
locator.full_lines_range(*range),
)));
}
diagnostics.push(diagnostic);
}
Some(diagnostic)
} else {
None
}
diagnostics
}

View File

@@ -0,0 +1,3 @@
pub(crate) use commented_out_code::{commented_out_code, CommentedOutCode};
mod commented_out_code;

View File

@@ -0,0 +1,8 @@
use ruff_python_semantic::model::SemanticModel;
use rustpython_parser::ast::Expr;
pub(super) fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool {
model
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["sys", target])
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-2020](https://pypi.org/project/flake8-2020/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]
@@ -6,7 +7,6 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::registry::Rule;

View File

@@ -7,25 +7,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
#[violation]
pub struct SysVersionSlice3;
impl Violation for SysVersionSlice3 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
}
}
#[violation]
pub struct SysVersion2;
impl Violation for SysVersion2 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[2]` referenced (python3.10), use `sys.version_info`")
}
}
use super::super::helpers::is_sys;
#[violation]
pub struct SysVersionCmpStr3;
@@ -47,16 +29,6 @@ impl Violation for SysVersionInfo0Eq3 {
}
}
#[violation]
pub struct SixPY3;
impl Violation for SixPY3 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`six.PY3` referenced (python4), use `not six.PY2`")
}
}
#[violation]
pub struct SysVersionInfo1CmpInt;
@@ -83,16 +55,6 @@ impl Violation for SysVersionInfoMinorCmpInt {
}
}
#[violation]
pub struct SysVersion0;
impl Violation for SysVersion0 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[0]` referenced (python10), use `sys.version_info`")
}
}
#[violation]
pub struct SysVersionCmpStr10;
@@ -103,80 +65,11 @@ impl Violation for SysVersionCmpStr10 {
}
}
#[violation]
pub struct SysVersionSlice1;
impl Violation for SysVersionSlice1 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
}
}
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
checker
.ctx
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["sys", target])
}
/// YTT101, YTT102, YTT301, YTT303
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(checker, value, "version") {
match slice {
Expr::Slice(ast::ExprSlice {
lower: None,
upper: Some(upper),
step: None,
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) = upper.as_ref()
{
if *i == BigInt::from(1)
&& checker.settings.rules.enabled(Rule::SysVersionSlice1)
{
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3)
&& checker.settings.rules.enabled(Rule::SysVersionSlice3)
{
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) => {
if *i == BigInt::from(2) && checker.settings.rules.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.settings.rules.enabled(Rule::SysVersion0)
{
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));
}
}
_ => {}
}
}
}
/// YTT103, YTT201, YTT203, YTT204, YTT302
pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) {
match left {
Expr::Subscript(ast::ExprSubscript { value, slice, .. })
if is_sys(checker, value, "version_info") =>
if is_sys(checker.semantic_model(), value, "version_info") =>
{
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
@@ -192,9 +85,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
})],
) = (ops, comparators)
{
if *n == BigInt::from(3)
&& checker.settings.rules.enabled(Rule::SysVersionInfo0Eq3)
{
if *n == BigInt::from(3) && checker.enabled(Rule::SysVersionInfo0Eq3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
@@ -209,7 +100,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
})],
) = (ops, comparators)
{
if checker.settings.rules.enabled(Rule::SysVersionInfo1CmpInt) {
if checker.enabled(Rule::SysVersionInfo1CmpInt) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo1CmpInt, left.range()));
@@ -220,7 +111,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
}
Expr::Attribute(ast::ExprAttribute { value, attr, .. })
if is_sys(checker, value, "version_info") && attr == "minor" =>
if is_sys(checker.semantic_model(), value, "version_info") && attr == "minor" =>
{
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
@@ -230,11 +121,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
})],
) = (ops, comparators)
{
if checker
.settings
.rules
.enabled(Rule::SysVersionInfoMinorCmpInt)
{
if checker.enabled(Rule::SysVersionInfoMinorCmpInt) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfoMinorCmpInt, left.range()));
@@ -245,7 +132,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
_ => {}
}
if is_sys(checker, left, "version") {
if is_sys(checker.semantic_model(), left, "version") {
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Expr::Constant(ast::ExprConstant {
@@ -255,12 +142,12 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
) = (ops, comparators)
{
if s.len() == 1 {
if checker.settings.rules.enabled(Rule::SysVersionCmpStr10) {
if checker.enabled(Rule::SysVersionCmpStr10) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr10, left.range()));
}
} else if checker.settings.rules.enabled(Rule::SysVersionCmpStr3) {
} else if checker.enabled(Rule::SysVersionCmpStr3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr3, left.range()));
@@ -268,16 +155,3 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
}
}
}
/// YTT202
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if checker
.ctx
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"])
{
checker
.diagnostics
.push(Diagnostic::new(SixPY3, expr.range()));
}
}

View File

@@ -0,0 +1,12 @@
pub(crate) use compare::{
compare, SysVersionCmpStr10, SysVersionCmpStr3, SysVersionInfo0Eq3, SysVersionInfo1CmpInt,
SysVersionInfoMinorCmpInt,
};
pub(crate) use name_or_attribute::{name_or_attribute, SixPY3};
pub(crate) use subscript::{
subscript, SysVersion0, SysVersion2, SysVersionSlice1, SysVersionSlice3,
};
mod compare;
mod name_or_attribute;
mod subscript;

View File

@@ -0,0 +1,29 @@
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct SixPY3;
impl Violation for SixPY3 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`six.PY3` referenced (python4), use `not six.PY2`")
}
}
/// YTT202
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if checker
.semantic_model()
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"])
{
checker
.diagnostics
.push(Diagnostic::new(SixPY3, expr.range()));
}
}

View File

@@ -0,0 +1,96 @@
use num_bigint::BigInt;
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::flake8_2020::helpers::is_sys;
#[violation]
pub struct SysVersionSlice3;
impl Violation for SysVersionSlice3 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
}
}
#[violation]
pub struct SysVersion2;
impl Violation for SysVersion2 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[2]` referenced (python3.10), use `sys.version_info`")
}
}
#[violation]
pub struct SysVersion0;
impl Violation for SysVersion0 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[0]` referenced (python10), use `sys.version_info`")
}
}
#[violation]
pub struct SysVersionSlice1;
impl Violation for SysVersionSlice1 {
#[derive_message_formats]
fn message(&self) -> String {
format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
}
}
/// YTT101, YTT102, YTT301, YTT303
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(checker.semantic_model(), value, "version") {
match slice {
Expr::Slice(ast::ExprSlice {
lower: None,
upper: Some(upper),
step: None,
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) = upper.as_ref()
{
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(i),
..
}) => {
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));
}
}
_ => {}
}
}
}

View File

@@ -3,8 +3,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Stmt};
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::definition::{Definition, Member, MemberKind};
use crate::checkers::ast::Checker;
use ruff_python_semantic::model::SemanticModel;
pub(super) fn match_function_def(
stmt: &Stmt,
@@ -37,14 +36,14 @@ pub(super) fn match_function_def(
}
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name(checker: &Checker, definition: &Definition) -> Option<String> {
pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> Option<String> {
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(&checker.ctx, cast::decorator_list(stmt)) {
if visibility::is_overload(model, cast::decorator_list(stmt)) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
@@ -58,7 +57,7 @@ pub(crate) fn overloaded_name(checker: &Checker, definition: &Definition) -> Opt
/// Return `true` if the definition is the implementation for an overloaded
/// function.
pub(crate) fn is_overload_impl(
checker: &Checker,
model: &SemanticModel,
definition: &Definition,
overloaded_name: &str,
) -> bool {
@@ -68,7 +67,7 @@ pub(crate) fn is_overload_impl(
..
}) = definition
{
if visibility::is_overload(&checker.ctx, cast::decorator_list(stmt)) {
if visibility::is_overload(model, cast::decorator_list(stmt)) {
false
} else {
let (name, ..) = match_function_def(stmt);

View File

@@ -8,9 +8,9 @@ pub mod settings;
mod tests {
use std::path::Path;
use crate::assert_messages;
use anyhow::Result;
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;

View File

@@ -8,13 +8,14 @@ use ruff_python_ast::{cast, helpers};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::analyze::visibility::Visibility;
use ruff_python_semantic::definition::{Definition, Member, MemberKind};
use ruff_python_semantic::model::SemanticModel;
use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use super::fixes;
use super::helpers::match_function_def;
use super::super::fixes;
use super::super::helpers::match_function_def;
/// ## What it does
/// Checks that function arguments have type annotations.
@@ -430,7 +431,7 @@ fn is_none_returning(body: &[Stmt]) -> bool {
/// ANN401
fn check_dynamically_typed<F>(
checker: &Checker,
model: &SemanticModel,
annotation: &Expr,
func: F,
diagnostics: &mut Vec<Diagnostic>,
@@ -438,7 +439,7 @@ fn check_dynamically_typed<F>(
) where
F: FnOnce() -> String,
{
if !is_overridden && checker.ctx.match_typing_expr(annotation, "Any") {
if !is_overridden && model.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
@@ -479,7 +480,7 @@ pub(crate) fn definition(
// unless configured to suppress ANN* for declarations that are fully untyped.
let mut diagnostics = Vec::new();
let is_overridden = visibility::is_override(&checker.ctx, decorator_list);
let is_overridden = visibility::is_override(checker.semantic_model(), decorator_list);
// ANN001, ANN401
for arg in args
@@ -490,16 +491,20 @@ pub(crate) fn definition(
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method && !visibility::is_staticmethod(&checker.ctx, cast::decorator_list(stmt)),
is_method
&& !visibility::is_staticmethod(
checker.semantic_model(),
cast::decorator_list(stmt),
),
),
)
{
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.annotation {
has_any_typed_arg = true;
if checker.settings.rules.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) {
check_dynamically_typed(
checker,
checker.semantic_model(),
annotation,
|| arg.arg.to_string(),
&mut diagnostics,
@@ -510,11 +515,7 @@ pub(crate) fn definition(
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.arg))
{
if checker
.settings
.rules
.enabled(Rule::MissingTypeFunctionArgument)
{
if checker.enabled(Rule::MissingTypeFunctionArgument) {
diagnostics.push(Diagnostic::new(
MissingTypeFunctionArgument {
name: arg.arg.to_string(),
@@ -531,10 +532,10 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) {
let name = &arg.arg;
check_dynamically_typed(
checker,
checker.semantic_model(),
expr,
|| format!("*{name}"),
&mut diagnostics,
@@ -546,7 +547,7 @@ pub(crate) fn definition(
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.arg))
{
if checker.settings.rules.enabled(Rule::MissingTypeArgs) {
if checker.enabled(Rule::MissingTypeArgs) {
diagnostics.push(Diagnostic::new(
MissingTypeArgs {
name: arg.arg.to_string(),
@@ -563,10 +564,10 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) {
let name = &arg.arg;
check_dynamically_typed(
checker,
checker.semantic_model(),
expr,
|| format!("**{name}"),
&mut diagnostics,
@@ -578,7 +579,7 @@ pub(crate) fn definition(
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.arg))
{
if checker.settings.rules.enabled(Rule::MissingTypeKwargs) {
if checker.enabled(Rule::MissingTypeKwargs) {
diagnostics.push(Diagnostic::new(
MissingTypeKwargs {
name: arg.arg.to_string(),
@@ -591,11 +592,14 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(&checker.ctx, cast::decorator_list(stmt)) {
if is_method
&& !visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt))
{
if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) {
if arg.annotation.is_none() {
if visibility::is_classmethod(&checker.ctx, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(Rule::MissingTypeCls) {
if visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt))
{
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
name: arg.arg.to_string(),
@@ -604,7 +608,7 @@ pub(crate) fn definition(
));
}
} else {
if checker.settings.rules.enabled(Rule::MissingTypeSelf) {
if checker.enabled(Rule::MissingTypeSelf) {
diagnostics.push(Diagnostic::new(
MissingTypeSelf {
name: arg.arg.to_string(),
@@ -622,9 +626,9 @@ pub(crate) fn definition(
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker.settings.rules.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) {
check_dynamically_typed(
checker,
checker.semantic_model(),
expr,
|| name.to_string(),
&mut diagnostics,
@@ -636,12 +640,10 @@ pub(crate) fn definition(
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(&checker.ctx, cast::decorator_list(stmt)) {
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypeClassMethod)
{
if is_method
&& visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt))
{
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
@@ -649,13 +651,10 @@ pub(crate) fn definition(
helpers::identifier_range(stmt, checker.locator),
));
}
} else if is_method && visibility::is_staticmethod(&checker.ctx, cast::decorator_list(stmt))
} else if is_method
&& visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt))
{
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypeStaticMethod)
{
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
@@ -666,11 +665,7 @@ pub(crate) fn definition(
} else if is_method && visibility::is_init(name) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypeSpecialMethod)
{
if checker.enabled(Rule::MissingReturnTypeSpecialMethod) {
if !(checker.settings.flake8_annotations.mypy_init_return && has_any_typed_arg) {
let mut diagnostic = Diagnostic::new(
MissingReturnTypeSpecialMethod {
@@ -688,11 +683,7 @@ pub(crate) fn definition(
}
}
} else if is_method && visibility::is_magic(name) {
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypeSpecialMethod)
{
if checker.enabled(Rule::MissingReturnTypeSpecialMethod) {
let mut diagnostic = Diagnostic::new(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
@@ -713,11 +704,7 @@ pub(crate) fn definition(
} else {
match visibility {
Visibility::Public => {
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction)
{
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
@@ -727,11 +714,7 @@ pub(crate) fn definition(
}
}
Visibility::Private => {
if checker
.settings
.rules
.enabled(Rule::MissingReturnTypePrivateFunction)
{
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
diagnostics.push(Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),

View File

@@ -0,0 +1,8 @@
pub(crate) use definition::{
definition, AnyType, MissingReturnTypeClassMethod, MissingReturnTypePrivateFunction,
MissingReturnTypeSpecialMethod, MissingReturnTypeStaticMethod,
MissingReturnTypeUndocumentedPublicFunction, MissingTypeArgs, MissingTypeCls,
MissingTypeFunctionArgument, MissingTypeKwargs, MissingTypeSelf,
};
mod definition;

View File

@@ -0,0 +1,18 @@
use ruff_python_semantic::{
model::SemanticModel,
scope::{FunctionDef, ScopeKind},
};
/// Return `true` if the [`SemanticModel`] is inside an async function definition.
pub(crate) fn in_async_function(model: &SemanticModel) -> bool {
model
.scopes()
.find_map(|scope| {
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
Some(*async_)
} else {
None
}
})
.unwrap_or(false)
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]

View File

@@ -1,225 +0,0 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::context::Context;
use ruff_python_semantic::scope::{FunctionDef, ScopeKind};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not contain blocking HTTP calls.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking HTTP call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// HTTP response, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking HTTP call, use an asynchronous HTTP client
/// library such as `aiohttp` or `httpx`.
///
/// ## Example
/// ```python
/// async def fetch():
/// urllib.request.urlopen("https://example.com/foo/bar").read()
/// ```
///
/// Use instead:
/// ```python
/// async def fetch():
/// async with aiohttp.ClientSession() as session:
/// async with session.get("https://example.com/foo/bar") as resp:
/// ...
/// ```
#[violation]
pub struct BlockingHttpCallInAsyncFunction;
impl Violation for BlockingHttpCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call blocking HTTP methods")
}
}
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
&["urllib", "request", "urlopen"],
&["httpx", "get"],
&["httpx", "post"],
&["httpx", "delete"],
&["httpx", "patch"],
&["httpx", "put"],
&["httpx", "head"],
&["httpx", "connect"],
&["httpx", "options"],
&["httpx", "trace"],
&["requests", "get"],
&["requests", "post"],
&["requests", "delete"],
&["requests", "patch"],
&["requests", "put"],
&["requests", "head"],
&["requests", "connect"],
&["requests", "options"],
&["requests", "trace"],
];
/// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if BLOCKING_HTTP_CALLS.contains(&call_path.as_slice()) {
checker.diagnostics.push(Diagnostic::new(
BlockingHttpCallInAsyncFunction,
func.range(),
));
}
}
}
}
}
/// ## What it does
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
/// or `subprocess` 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():
/// time.sleep(1000)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// await asyncio.sleep(1000)
/// ```
#[violation]
pub struct OpenSleepOrSubprocessInAsyncFunction;
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
}
}
const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
&["", "open"],
&["time", "sleep"],
&["subprocess", "run"],
&["subprocess", "Popen"],
// Deprecated subprocess calls:
&["subprocess", "call"],
&["subprocess", "check_call"],
&["subprocess", "check_output"],
&["subprocess", "getoutput"],
&["subprocess", "getstatusoutput"],
&["os", "wait"],
&["os", "wait3"],
&["os", "wait4"],
&["os", "waitid"],
&["os", "waitpid"],
];
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&call_path.as_slice()) {
checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction,
func.range(),
));
}
}
}
}
}
/// ## 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")
}
}
const UNSAFE_OS_METHODS: &[&[&str]] = &[
&["os", "popen"],
&["os", "posix_spawn"],
&["os", "posix_spawnp"],
&["os", "spawnl"],
&["os", "spawnle"],
&["os", "spawnlp"],
&["os", "spawnlpe"],
&["os", "spawnv"],
&["os", "spawnve"],
&["os", "spawnvp"],
&["os", "spawnvpe"],
&["os", "system"],
];
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if UNSAFE_OS_METHODS.contains(&call_path.as_slice()) {
checker
.diagnostics
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range()));
}
}
}
}
}
/// Return `true` if the [`Context`] is inside an async function definition.
fn in_async_function(context: &Context) -> bool {
context
.scopes()
.find_map(|scope| {
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
Some(*async_)
} else {
None
}
})
.unwrap_or(false)
}

View File

@@ -0,0 +1,83 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::in_async_function;
/// ## What it does
/// Checks that async functions do not contain blocking HTTP calls.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking HTTP call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// HTTP response, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking HTTP call, use an asynchronous HTTP client
/// library such as `aiohttp` or `httpx`.
///
/// ## Example
/// ```python
/// async def fetch():
/// urllib.request.urlopen("https://example.com/foo/bar").read()
/// ```
///
/// Use instead:
/// ```python
/// async def fetch():
/// async with aiohttp.ClientSession() as session:
/// async with session.get("https://example.com/foo/bar") as resp:
/// ...
/// ```
#[violation]
pub struct BlockingHttpCallInAsyncFunction;
impl Violation for BlockingHttpCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call blocking HTTP methods")
}
}
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
&["urllib", "request", "urlopen"],
&["httpx", "get"],
&["httpx", "post"],
&["httpx", "delete"],
&["httpx", "patch"],
&["httpx", "put"],
&["httpx", "head"],
&["httpx", "connect"],
&["httpx", "options"],
&["httpx", "trace"],
&["requests", "get"],
&["requests", "post"],
&["requests", "delete"],
&["requests", "patch"],
&["requests", "put"],
&["requests", "head"],
&["requests", "connect"],
&["requests", "options"],
&["requests", "trace"],
];
/// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(checker.semantic_model()) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let call_path = checker.semantic_model().resolve_call_path(func);
let is_blocking =
call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice()));
if is_blocking {
checker.diagnostics.push(Diagnostic::new(
BlockingHttpCallInAsyncFunction,
func.range(),
));
}
}
}
}

View File

@@ -0,0 +1,75 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::in_async_function;
/// ## 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")
}
}
const UNSAFE_OS_METHODS: &[&[&str]] = &[
&["os", "popen"],
&["os", "posix_spawn"],
&["os", "posix_spawnp"],
&["os", "spawnl"],
&["os", "spawnle"],
&["os", "spawnlp"],
&["os", "spawnlpe"],
&["os", "spawnv"],
&["os", "spawnve"],
&["os", "spawnvp"],
&["os", "spawnvpe"],
&["os", "system"],
];
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(checker.semantic_model()) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let is_unsafe_os_method = checker
.semantic_model()
.resolve_call_path(func)
.map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice()));
if is_unsafe_os_method {
checker
.diagnostics
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range()));
}
}
}
}

View File

@@ -0,0 +1,9 @@
pub(crate) use blocking_http_call::{blocking_http_call, BlockingHttpCallInAsyncFunction};
pub(crate) use blocking_os_call::{blocking_os_call, BlockingOsCallInAsyncFunction};
pub(crate) use open_sleep_or_subprocess_call::{
open_sleep_or_subprocess_call, OpenSleepOrSubprocessInAsyncFunction,
};
mod blocking_http_call;
mod blocking_os_call;
mod open_sleep_or_subprocess_call;

View File

@@ -0,0 +1,81 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::in_async_function;
/// ## What it does
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
/// or `subprocess` 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():
/// time.sleep(1000)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// await asyncio.sleep(1000)
/// ```
#[violation]
pub struct OpenSleepOrSubprocessInAsyncFunction;
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
}
}
const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
&["", "open"],
&["time", "sleep"],
&["subprocess", "run"],
&["subprocess", "Popen"],
// Deprecated subprocess calls:
&["subprocess", "call"],
&["subprocess", "check_call"],
&["subprocess", "check_output"],
&["subprocess", "getoutput"],
&["subprocess", "getstatusoutput"],
&["os", "wait"],
&["os", "wait3"],
&["os", "wait4"],
&["os", "waitid"],
&["os", "waitpid"],
];
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(checker.semantic_model()) {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let is_open_sleep_or_subprocess_call = checker
.semantic_model()
.resolve_call_path(func)
.map_or(false, |path| {
OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice())
});
if is_open_sleep_or_subprocess_call {
checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction,
func.range(),
));
}
}
}
}

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