Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c50c311c | ||
|
|
252506f8ed | ||
|
|
f4572fe40b | ||
|
|
8c9215489e | ||
|
|
dcd2bfaab7 | ||
|
|
f0e173d9fd | ||
|
|
f4f1b1d0ee | ||
|
|
edc6c4058f | ||
|
|
4233f6ec91 | ||
|
|
fcdc7bdd33 | ||
|
|
86ced3516b | ||
|
|
6943beee66 | ||
|
|
85f094f592 | ||
|
|
17d938f078 | ||
|
|
5cedf0f724 | ||
|
|
38297c08b4 | ||
|
|
30e90838d0 | ||
|
|
040fb9cef4 | ||
|
|
8961d8eb6f | ||
|
|
31bddef98f | ||
|
|
a59d252246 | ||
|
|
5b9d4f18ae | ||
|
|
c6a760e298 | ||
|
|
3644695bf2 | ||
|
|
b1d01b1950 | ||
|
|
4e84e8a8e2 | ||
|
|
a256fdb9f4 | ||
|
|
7479dfd815 | ||
|
|
ba4c0a21fa | ||
|
|
73e179ffab | ||
|
|
3cbaaa4795 | ||
|
|
2681c0e633 | ||
|
|
f3bdd2e7be | ||
|
|
652c644c2a | ||
|
|
04d273bcc7 | ||
|
|
154439728a | ||
|
|
1ddc577204 | ||
|
|
74effb40b9 | ||
|
|
6c3724ab98 | ||
|
|
3b8121379d | ||
|
|
5ba47c3302 | ||
|
|
b613460fe5 | ||
|
|
daadd24bde | ||
|
|
9308e939f4 | ||
|
|
04c9348de0 | ||
|
|
2d3766d928 | ||
|
|
550b643e33 | ||
|
|
cbe344f4d5 | ||
|
|
063431cb0f | ||
|
|
c6e5fed658 | ||
|
|
f73b398776 | ||
|
|
55c4020ba9 | ||
|
|
d70f899f71 | ||
|
|
19c4b7bee6 | ||
|
|
3238743a7b | ||
|
|
f22c269ccf | ||
|
|
8ca3977602 | ||
|
|
6db05d8cc6 | ||
|
|
fc63c6f2e2 | ||
|
|
f7f5bc9085 | ||
|
|
6b85430a14 | ||
|
|
a68c865010 | ||
|
|
fe7f2e2e4d | ||
|
|
0a3cf8ba11 | ||
|
|
bf5b463c0d | ||
|
|
6aa9900c03 | ||
|
|
9e21414294 | ||
|
|
bb4e674415 | ||
|
|
b42ff08612 | ||
|
|
03fb62c174 | ||
|
|
2dfc645ea9 | ||
|
|
fe8e2bb237 | ||
|
|
a9ed8d5391 | ||
|
|
41a681531d | ||
|
|
837e70677b | ||
|
|
7ebe372122 | ||
|
|
625849b846 | ||
|
|
32f1edc555 | ||
|
|
2f35099f81 | ||
|
|
ce8fd31a8f | ||
|
|
fdb241cad2 | ||
|
|
ab303f4e09 | ||
|
|
15cb21a6f4 | ||
|
|
2e2ba2cb16 | ||
|
|
d4c0a41b00 | ||
|
|
8702b5a40a | ||
|
|
bab818e801 |
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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? -->
|
||||
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -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"
|
||||
|
||||
4
.github/workflows/docs.yaml
vendored
4
.github/workflows/docs.yaml
vendored
@@ -1,9 +1,9 @@
|
||||
name: mkdocs
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
mkdocs:
|
||||
|
||||
2
.github/workflows/flake8-to-ruff.yaml
vendored
2
.github/workflows/flake8-to-ruff.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/playground.yaml
vendored
4
.github/workflows/playground.yaml
vendored
@@ -2,8 +2,8 @@ name: "[Playground] Release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
26
.github/workflows/release.yaml
vendored
26
.github/workflows/release.yaml
vendored
@@ -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
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -21,6 +21,7 @@ repos:
|
||||
- --disable
|
||||
- MD013 # line-length
|
||||
- MD033 # no-inline-html
|
||||
- MD041 # first-line-h1
|
||||
- --
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
|
||||
@@ -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
45
Cargo.lock
generated
@@ -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",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -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"] }
|
||||
|
||||
12
README.md
12
README.md
@@ -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))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.267"
|
||||
version = "0.0.270"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
3
crates/ruff/resources/test/fixtures/flake8_bandit/S601.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/flake8_bandit/S601.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import paramiko
|
||||
|
||||
paramiko.exec_command('something; really; unsafe')
|
||||
@@ -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()
|
||||
|
||||
65
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.py
vendored
Normal file
65
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.py
vendored
Normal 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():
|
||||
...
|
||||
|
||||
|
||||
...
|
||||
56
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.pyi
vendored
Normal file
56
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.pyi
vendored
Normal 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(): ...
|
||||
|
||||
...
|
||||
@@ -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!")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -155,3 +155,19 @@ def f():
|
||||
if check(x):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ":
|
||||
if x.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
# OK (too long)
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9":
|
||||
if x.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -171,3 +171,19 @@ def f():
|
||||
if x > y:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9":
|
||||
if x.isdigit():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# OK (too long)
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß90":
|
||||
if x.isdigit():
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -90,3 +90,13 @@ with (
|
||||
D() as d,
|
||||
):
|
||||
print("hello")
|
||||
|
||||
# SIM117 (auto-fixable)
|
||||
with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
print("hello")
|
||||
|
||||
# SIM117 (not auto-fixable too long)
|
||||
with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a:
|
||||
with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
print("hello")
|
||||
@@ -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💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
|
||||
###
|
||||
# Negative cases
|
||||
###
|
||||
|
||||
# OK (too long)
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß"
|
||||
|
||||
# OK (false negative)
|
||||
if not key in a_dict:
|
||||
var = "default"
|
||||
|
||||
@@ -146,3 +146,7 @@ def f():
|
||||
import pandas as pd
|
||||
|
||||
x = dict[pd.DataFrame, pd.DataFrame]
|
||||
|
||||
|
||||
def f():
|
||||
import pandas as pd
|
||||
|
||||
@@ -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💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ
|
||||
# The next import doesn't fit on one line.
|
||||
from h import i # 012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9
|
||||
|
||||
4
crates/ruff/resources/test/fixtures/isort/required_imports/off.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/isort/required_imports/off.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# isort: off
|
||||
|
||||
x = 1
|
||||
# isort: on
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@ if x > 0:
|
||||
else:
|
||||
import e
|
||||
|
||||
y = x + 1
|
||||
__some__magic = 1
|
||||
|
||||
import f
|
||||
|
||||
|
||||
11
crates/ruff/resources/test/fixtures/pycodestyle/E501_2.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/pycodestyle/E501_2.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
31
crates/ruff/resources/test/fixtures/pycodestyle/W505_utf_8.py
vendored
Normal file
31
crates/ruff/resources/test/fixtures/pycodestyle/W505_utf_8.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Here's a top-level ß9💣2ℝing that's over theß9💣2ℝ."""
|
||||
|
||||
|
||||
def f1():
|
||||
"""Here's a ß9💣2ℝing 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💣2ℝing.")
|
||||
|
||||
|
||||
"This is also considered a ß9💣2ℝing, and is over theß9💣2ℝ."
|
||||
|
||||
|
||||
def f2():
|
||||
"""Here's a multi-line ß9💣2ℝing.
|
||||
|
||||
It's over theß9💣2ℝ on this line, which isn't the first line in the ß9💣2ℝing.
|
||||
"""
|
||||
|
||||
|
||||
def f3():
|
||||
"""Here's a multi-line ß9💣2ℝing.
|
||||
|
||||
It's over theß9💣2ℝ on this line, which isn't the first line in the ß9💣2ℝing."""
|
||||
10
crates/ruff/resources/test/fixtures/pyflakes/F401_12.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/pyflakes/F401_12.py
vendored
Normal 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]
|
||||
12
crates/ruff/resources/test/fixtures/pyflakes/F401_13.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/pyflakes/F401_13.py
vendored
Normal 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
|
||||
8
crates/ruff/resources/test/fixtures/pyflakes/F401_14.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/pyflakes/F401_14.py
vendored
Normal 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]"
|
||||
4
crates/ruff/resources/test/fixtures/pyflakes/F811_23.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/pyflakes/F811_23.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Test that shadowing an explicit re-export produces a warning."""
|
||||
|
||||
import foo as foo
|
||||
import bar as foo
|
||||
5
crates/ruff/resources/test/fixtures/pyflakes/F811_24.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pyflakes/F811_24.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test that shadowing a `__future__` import does not produce a warning."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import annotations
|
||||
11
crates/ruff/resources/test/fixtures/pylint/duplicate_value.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/pylint/duplicate_value.py
vendored
Normal 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"}
|
||||
19
crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py
vendored
Normal 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)
|
||||
28
crates/ruff/resources/test/fixtures/pyupgrade/UP032_2.py
vendored
Normal file
28
crates/ruff/resources/test/fixtures/pyupgrade/UP032_2.py
vendored
Normal 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)))
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "Р усский"
|
||||
|
||||
23
crates/ruff/resources/test/fixtures/ruff/noqa.py
vendored
Normal file
23
crates/ruff/resources/test/fixtures/ruff/noqa.py
vendored
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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![]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
165
crates/ruff/src/line_width.rs
Normal file
165
crates/ruff/src/line_width.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}\"");
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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", &[]));
|
||||
|
||||
@@ -7,7 +7,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
3
crates/ruff/src/rules/eradicate/rules/mod.rs
Normal file
3
crates/ruff/src/rules/eradicate/rules/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(crate) use commented_out_code::{commented_out_code, CommentedOutCode};
|
||||
|
||||
mod commented_out_code;
|
||||
8
crates/ruff/src/rules/flake8_2020/helpers.rs
Normal file
8
crates/ruff/src/rules/flake8_2020/helpers.rs
Normal 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])
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
12
crates/ruff/src/rules/flake8_2020/rules/mod.rs
Normal file
12
crates/ruff/src/rules/flake8_2020/rules/mod.rs
Normal 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;
|
||||
29
crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs
Normal file
29
crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
96
crates/ruff/src/rules/flake8_2020/rules/subscript.rs
Normal file
96
crates/ruff/src/rules/flake8_2020/rules/subscript.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
8
crates/ruff/src/rules/flake8_annotations/rules/mod.rs
Normal file
8
crates/ruff/src/rules/flake8_annotations/rules/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub(crate) use definition::{
|
||||
definition, AnyType, MissingReturnTypeClassMethod, MissingReturnTypePrivateFunction,
|
||||
MissingReturnTypeSpecialMethod, MissingReturnTypeStaticMethod,
|
||||
MissingReturnTypeUndocumentedPublicFunction, MissingTypeArgs, MissingTypeCls,
|
||||
MissingTypeFunctionArgument, MissingTypeKwargs, MissingTypeSelf,
|
||||
};
|
||||
|
||||
mod definition;
|
||||
18
crates/ruff/src/rules/flake8_async/helpers.rs
Normal file
18
crates/ruff/src/rules/flake8_async/helpers.rs
Normal 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)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs
Normal file
75
crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/ruff/src/rules/flake8_async/rules/mod.rs
Normal file
9
crates/ruff/src/rules/flake8_async/rules/mod.rs
Normal 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;
|
||||
@@ -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
Reference in New Issue
Block a user