Compare commits

...

44 Commits

Author SHA1 Message Date
Charlie Marsh
4b3488b5b2 Use SmallVec for binding references 2023-05-24 10:33:51 -04:00
konstin
17d938f078 Add Checker::any_enabled shortcut (#4630)
Add Checker::any_enabled shortcut

 ## Summary

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

 ## Test Plan

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

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

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

* Make ecosystem all check more generic

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

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

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

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

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

* delete old file

* Update maturin to 1.0

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

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

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

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

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

* Use tempdir for schemastore

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

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

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

* Keep github template intact

* Remove the need for ripgrep

* sort output
2023-05-22 15:23:25 +02:00
Micha Reiser
cbe344f4d5 Rename Checker::model to semantic_model (#4573) 2023-05-22 15:14:30 +02:00
Micha Reiser
063431cb0f Upgrade RustPython (#4576) 2023-05-22 14:50:49 +02:00
Evan Rittenhouse
c6e5fed658 Replace token iteration with Indexer/Locator lookups for relevant rules (#4513) 2023-05-22 09:56:19 +02:00
Charlie Marsh
f73b398776 Reduce visibility of more functions, structs, and fields (#4570) 2023-05-22 03:36:48 +00:00
Charlie Marsh
55c4020ba9 Remove regex for noqa code splitting (#4569) 2023-05-21 23:20:49 -04:00
Charlie Marsh
d70f899f71 Use SemanticModel in lieu of Checker in more methods (#4568) 2023-05-22 02:58:47 +00:00
Charlie Marsh
19c4b7bee6 Rename ruff_python_semantic's Context struct to SemanticModel (#4565) 2023-05-22 02:35:03 +00:00
Jonathan Plasse
3238743a7b Fix Flake8Todo typo (#4566) 2023-05-21 16:32:13 -04:00
Charlie Marsh
f22c269ccf Point LSP, VS Code, and pre-commut URLs to Astral org (#4562) 2023-05-21 15:27:35 -04:00
454 changed files with 10602 additions and 4535 deletions

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

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

View File

@@ -3,7 +3,7 @@ name: "[ruff] Release"
on:
workflow_dispatch:
release:
types: [published]
types: [ published ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -406,9 +406,6 @@ jobs:
run: |
pip install --upgrade twine
twine upload --skip-existing *
- name: "Update pre-commit mirror"
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "pypi_release"}'
- uses: actions/download-artifact@v3
with:
name: binaries
@@ -417,3 +414,22 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: binaries/*
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Release
runs-on: ubuntu-latest
needs: release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v6
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'astral-sh',
repo: 'ruff-pre-commit',
workflow_id: 'main.yml',
ref: 'main',
})

4
.gitignore vendored
View File

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

View File

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

23
Cargo.lock generated
View File

@@ -1780,6 +1780,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-width",
"unicode_names2",
]
[[package]]
@@ -1903,6 +1904,14 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "ruff_index"
version = "0.0.0"
dependencies = [
"ruff_macros",
"static_assertions",
]
[[package]]
name = "ruff_macros"
version = "0.0.0"
@@ -1929,6 +1938,7 @@ dependencies = [
"once_cell",
"ruff_text_size",
"rustc-hash",
"rustpython-ast",
"rustpython-literal",
"rustpython-parser",
"serde",
@@ -1963,6 +1973,7 @@ dependencies = [
"bitflags 2.3.1",
"is-macro",
"nohash-hasher",
"ruff_index",
"ruff_python_ast",
"ruff_python_stdlib",
"ruff_text_size",
@@ -2000,7 +2011,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"schemars",
"serde",
@@ -2071,7 +2082,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"num-bigint",
@@ -2082,7 +2093,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"bitflags 2.3.1",
"itertools",
@@ -2094,7 +2105,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2106,7 +2117,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"anyhow",
"is-macro",
@@ -2129,7 +2140,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"ruff_text_size",

View File

@@ -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 = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["all-nodes-with-ranges"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }

View File

@@ -33,7 +33,7 @@ An extremely fast Python linter, written in Rust.
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party editor integrations for [VS Code](https://github.com/charliermarsh/ruff-vscode) and [more](https://github.com/charliermarsh/ruff-lsp)
- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
@@ -135,15 +135,15 @@ ruff check path/to/code/to/file.py # Lint `file.py`
Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.269'
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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -737,13 +737,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flynt, "002") => (RuleGroup::Unspecified, Rule::StaticJoinToFString),
// flake8-todos
(Flake8Todo, "001") => (RuleGroup::Unspecified, Rule::InvalidTodoTag),
(Flake8Todo, "002") => (RuleGroup::Unspecified, Rule::MissingTodoAuthor),
(Flake8Todo, "003") => (RuleGroup::Unspecified, Rule::MissingTodoLink),
(Flake8Todo, "004") => (RuleGroup::Unspecified, Rule::MissingTodoColon),
(Flake8Todo, "005") => (RuleGroup::Unspecified, Rule::MissingTodoDescription),
(Flake8Todo, "006") => (RuleGroup::Unspecified, Rule::InvalidTodoCapitalization),
(Flake8Todo, "007") => (RuleGroup::Unspecified, Rule::MissingSpaceAfterTodoColon),
(Flake8Todos, "001") => (RuleGroup::Unspecified, Rule::InvalidTodoTag),
(Flake8Todos, "002") => (RuleGroup::Unspecified, Rule::MissingTodoAuthor),
(Flake8Todos, "003") => (RuleGroup::Unspecified, Rule::MissingTodoLink),
(Flake8Todos, "004") => (RuleGroup::Unspecified, Rule::MissingTodoColon),
(Flake8Todos, "005") => (RuleGroup::Unspecified, Rule::MissingTodoDescription),
(Flake8Todos, "006") => (RuleGroup::Unspecified, Rule::InvalidTodoCapitalization),
(Flake8Todos, "007") => (RuleGroup::Unspecified, Rule::MissingSpaceAfterTodoColon),
_ => return None,
})

View File

@@ -1,8 +1,9 @@
use anyhow::{bail, Result};
use libcst_native::{
Attribute, Call, Comparison, Dict, Expr, Expression, FormattedString, FormattedStringContent,
FormattedStringExpression, Import, ImportAlias, ImportFrom, ImportNames, Module, Name,
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> {
@@ -19,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 {
@@ -44,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 {
@@ -67,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 {
@@ -135,10 +141,100 @@ pub(crate) fn match_formatted_string_expression<'a, 'b>(
}
}
pub(crate) fn match_name<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Name<'b>> {
pub(crate) fn match_name<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Name<'b>> {
if let Expression::Name(name) = expression {
Ok(name)
} else {
bail!("Expected Expression::Name")
}
}
pub(crate) fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> {
if let Some(arg) = call.args.first() {
Ok(arg)
} else {
bail!("Expected Arg")
}
}
pub(crate) fn match_generator_exp<'a, 'b>(
expression: &'a Expression<'b>,
) -> Result<&'a GeneratorExp<'b>> {
if let Expression::GeneratorExp(generator_exp) = expression {
Ok(generator_exp)
} else {
bail!("Expected Expression::GeneratorExp")
}
}
pub(crate) fn match_tuple<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Tuple<'b>> {
if let Expression::Tuple(tuple) = expression {
Ok(tuple)
} else {
bail!("Expected Expression::Tuple")
}
}
pub(crate) fn match_list_comp<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a ListComp<'b>> {
if let Expression::ListComp(list_comp) = expression {
Ok(list_comp)
} else {
bail!("Expected Expression::ListComp")
}
}
pub(crate) fn match_lambda<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Lambda<'b>> {
if let Expression::Lambda(lambda) = expression {
Ok(lambda)
} else {
bail!("Expected Expression::Lambda")
}
}
pub(crate) fn match_function_def<'a, 'b>(
statement: &'a mut Statement<'b>,
) -> Result<&'a mut FunctionDef<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::FunctionDef(function_def) = compound {
Ok(function_def)
} else {
bail!("Expected CompoundStatement::FunctionDef")
}
} else {
bail!("Expected Statement::Compound")
}
}
pub(crate) fn match_indented_block<'a, 'b>(
suite: &'a mut Suite<'b>,
) -> Result<&'a mut IndentedBlock<'b>> {
if let Suite::IndentedBlock(indented_block) = suite {
Ok(indented_block)
} else {
bail!("Expected Suite::IndentedBlock")
}
}
pub(crate) fn match_with<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut With<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::With(with) = compound {
Ok(with)
} else {
bail!("Expected CompoundStatement::With")
}
} else {
bail!("Expected Statement::Compound")
}
}
pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut If<'b>> {
if let Statement::Compound(compound) = statement {
if let CompoundStatement::If(if_) = compound {
Ok(if_)
} else {
bail!("Expected CompoundStatement::If")
}
} else {
bail!("Expected Statement::Compound")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
use std::io::Write;
use ruff_python_ast::source_code::SourceLocation;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
/// Generate error logging commands for Azure Pipelines format.
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
@@ -42,9 +44,10 @@ impl Emitter for AzureEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::AzureEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

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

View File

@@ -1,8 +1,10 @@
use std::io::Write;
use ruff_python_ast::source_code::SourceLocation;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
/// Generate error workflow command in GitHub Actions format.
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
@@ -57,9 +59,10 @@ impl Emitter for GithubEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GithubEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,14 +1,17 @@
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Write;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_python_ast::source_code::SourceLocation;
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
/// Generate JSON with violations in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
pub struct GitlabEmitter {
@@ -122,9 +125,10 @@ fn fingerprint(
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GitlabEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
use std::io::Write;
use std::path::Path;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
use ruff_python_ast::source_code::SourceLocation;
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::registry::AsRule;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
use ruff_python_ast::source_code::SourceLocation;
use std::io::Write;
use std::path::Path;
#[derive(Default)]
pub struct JunitEmitter;
@@ -83,9 +86,10 @@ impl Emitter for JunitEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::JunitEmitter;
use insta::assert_snapshot;
#[test]
fn output() {

View File

@@ -1,3 +1,25 @@
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;
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;
use crate::jupyter::JupyterIndex;
use crate::registry::AsRule;
mod azure;
mod diff;
mod github;
@@ -8,27 +30,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,
@@ -152,13 +153,15 @@ impl<'a> EmitterContext<'a> {
#[cfg(test)]
mod tests {
use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::SourceFileBuilder;
use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
pub(super) fn create_messages() -> Vec<Message> {
let fib = r#"import os

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
//! Registry of all [`Rule`] implementations.
mod rule_set;
use strum_macros::{AsRefStr, EnumIter};
use ruff_diagnostics::Violation;
use ruff_macros::RuleNamespace;
pub use rule_set::{RuleSet, RuleSetIterator};
use crate::codes::{self, RuleCodePrefix};
use crate::rules;
pub use rule_set::{RuleSet, RuleSetIterator};
mod rule_set;
ruff_macros::register_rules!(
// pycodestyle errors
@@ -812,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,
@@ -1003,6 +1003,7 @@ pub const INCOMPATIBLE_CODES: &[(Rule, Rule, &str); 2] = &[
#[cfg(test)]
mod tests {
use std::mem::size_of;
use strum::IntoEnumIterator;
use super::{Linter, Rule, RuleNamespace};

View File

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

View File

@@ -1,8 +1,6 @@
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;
@@ -47,24 +45,28 @@ fn is_standalone_comment(line: &str) -> bool {
/// ERA001
pub(crate) fn commented_out_code(
indexer: &Indexer,
locator: &Locator,
range: TextRange,
settings: &Settings,
) -> Option<Diagnostic> {
let line = locator.full_lines(range);
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
for range in indexer.comment_ranges() {
let line = locator.full_lines(*range);
if settings.rules.should_fix(Rule::CommentedOutCode) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
locator.full_lines_range(range),
)));
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
if settings.rules.should_fix(Rule::CommentedOutCode) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
locator.full_lines_range(*range),
)));
}
diagnostics.push(diagnostic);
}
Some(diagnostic)
} else {
None
}
diagnostics
}

View File

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

View File

@@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::model::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -113,16 +114,15 @@ impl Violation for SysVersionSlice1 {
}
}
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
checker
.ctx
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])
}
/// YTT101, YTT102, YTT301, YTT303
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(checker, value, "version") {
if is_sys(checker.semantic_model(), value, "version") {
match slice {
Expr::Slice(ast::ExprSlice {
lower: None,
@@ -135,15 +135,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
..
}) = upper.as_ref()
{
if *i == BigInt::from(1)
&& checker.settings.rules.enabled(Rule::SysVersionSlice1)
{
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3)
&& checker.settings.rules.enabled(Rule::SysVersionSlice3)
{
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
@@ -155,12 +151,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
value: Constant::Int(i),
..
}) => {
if *i == BigInt::from(2) && checker.settings.rules.enabled(Rule::SysVersion2) {
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.settings.rules.enabled(Rule::SysVersion0)
{
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));
@@ -176,7 +171,7 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
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 +187,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 +202,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 +213,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 +223,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 +234,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 +244,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()));
@@ -272,7 +261,7 @@ 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
.semantic_model()
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"])
{

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ 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::model::SemanticModel;
use ruff_python_semantic::scope::{FunctionDef, ScopeKind};
use crate::checkers::ast::Checker;
@@ -66,15 +66,17 @@ const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
/// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if in_async_function(checker.semantic_model()) {
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(),
));
}
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(),
));
}
}
}
@@ -133,15 +135,20 @@ const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if in_async_function(checker.semantic_model()) {
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(),
));
}
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(),
));
}
}
}
@@ -197,22 +204,25 @@ const UNSAFE_OS_METHODS: &[&[&str]] = &[
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if in_async_function(checker.semantic_model()) {
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()));
}
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()));
}
}
}
}
/// Return `true` if the [`Context`] is inside an async function definition.
fn in_async_function(context: &Context) -> bool {
context
/// Return `true` if the [`SemanticModel`] is inside an async function definition.
fn in_async_function(model: &SemanticModel) -> bool {
model
.scopes()
.find_map(|scope| {
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {

View File

@@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::{self, Constant, Expr};
use crate::checkers::ast::Checker;
use ruff_python_semantic::model::SemanticModel;
static PASSWORD_CANDIDATE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(^|_)(?i)(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)($|_)").unwrap()
@@ -22,26 +22,20 @@ pub(crate) fn matches_password_name(string: &str) -> bool {
PASSWORD_CANDIDATE_REGEX.is_match(string)
}
pub(crate) fn is_untyped_exception(type_: Option<&Expr>, checker: &Checker) -> bool {
pub(crate) fn is_untyped_exception(type_: Option<&Expr>, model: &SemanticModel) -> bool {
type_.map_or(true, |type_| {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ {
elts.iter().any(|type_| {
checker
.ctx
.resolve_call_path(type_)
.map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
})
} else {
checker
.ctx
.resolve_call_path(type_)
.map_or(false, |call_path| {
model.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
})
} else {
model.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
}
})
}

View File

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

View File

@@ -108,7 +108,7 @@ pub(crate) fn bad_file_permissions(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| call_path.as_slice() == ["os", "chmod"])
{

View File

@@ -1,6 +1,7 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::TextRange;
#[violation]
pub struct HardcodedBindAllInterfaces;

View File

@@ -60,7 +60,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
op: Operator::Add | Operator::Mod,
..
}) => {
let Some(parent) = checker.ctx.expr_parent() else {
let Some(parent) = checker.semantic_model().expr_parent() else {
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
}

View File

@@ -48,16 +48,21 @@ pub(crate) fn hashlib_insecure_hash_functions(
args: &[Expr],
keywords: &[Keyword],
) {
if let Some(hashlib_call) = checker.ctx.resolve_call_path(func).and_then(|call_path| {
if call_path.as_slice() == ["hashlib", "new"] {
Some(HashlibCall::New)
} else {
WEAK_HASHES
.iter()
.find(|hash| call_path.as_slice() == ["hashlib", hash])
.map(|hash| HashlibCall::WeakHash(hash))
}
}) {
if let Some(hashlib_call) =
checker
.semantic_model()
.resolve_call_path(func)
.and_then(|call_path| {
if call_path.as_slice() == ["hashlib", "new"] {
Some(HashlibCall::New)
} else {
WEAK_HASHES
.iter()
.find(|hash| call_path.as_slice() == ["hashlib", hash])
.map(|hash| HashlibCall::WeakHash(hash))
}
})
{
match hashlib_call {
HashlibCall::New => {
let call_args = SimpleCallArgs::new(args, keywords);

View File

@@ -37,7 +37,7 @@ pub(crate) fn jinja2_autoescape_false(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["jinja2", "Environment"]

View File

@@ -24,7 +24,7 @@ pub(crate) fn logging_config_insecure_listen(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["logging", "config", "listen"]

View File

@@ -18,7 +18,7 @@ impl Violation for ParamikoCall {
/// S601
pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["paramiko", "exec_command"]

View File

@@ -43,17 +43,21 @@ pub(crate) fn request_with_no_cert_validation(
args: &[Expr],
keywords: &[Keyword],
) {
if let Some(target) = checker.ctx.resolve_call_path(func).and_then(|call_path| {
if call_path.len() == 2 {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) {
return Some("requests");
if let Some(target) = checker
.semantic_model()
.resolve_call_path(func)
.and_then(|call_path| {
if call_path.len() == 2 {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) {
return Some("requests");
}
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) {
return Some("httpx");
}
}
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) {
return Some("httpx");
}
}
None
}) {
None
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(verify_arg) = call_args.keyword_argument("verify") {
if let Expr::Constant(ast::ExprConstant {

View File

@@ -34,7 +34,7 @@ pub(crate) fn request_without_timeout(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
HTTP_VERBS

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_semantic::context::Context;
use ruff_python_semantic::model::SemanticModel;
use crate::{
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
@@ -97,8 +97,8 @@ enum CallKind {
}
/// Return the [`CallKind`] of the given function call.
fn get_call_kind(func: &Expr, context: &Context) -> Option<CallKind> {
context
fn get_call_kind(func: &Expr, model: &SemanticModel) -> Option<CallKind> {
model
.resolve_call_path(func)
.and_then(|call_path| match call_path.as_slice() {
&[module, submodule] => match module {
@@ -138,12 +138,15 @@ struct ShellKeyword<'a> {
}
/// Return the `shell` keyword argument to the given function call, if any.
fn find_shell_keyword<'a>(ctx: &Context, keywords: &'a [Keyword]) -> Option<ShellKeyword<'a>> {
fn find_shell_keyword<'a>(
model: &SemanticModel,
keywords: &'a [Keyword],
) -> Option<ShellKeyword<'a>> {
keywords
.iter()
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "shell"))
.map(|keyword| ShellKeyword {
truthiness: Truthiness::from_expr(&keyword.value, |id| ctx.is_builtin(id)),
truthiness: Truthiness::from_expr(&keyword.value, |id| model.is_builtin(id)),
keyword,
})
}
@@ -181,21 +184,17 @@ pub(crate) fn shell_injection(
args: &[Expr],
keywords: &[Keyword],
) {
let call_kind = get_call_kind(func, &checker.ctx);
let call_kind = get_call_kind(func, checker.semantic_model());
if matches!(call_kind, Some(CallKind::Subprocess)) {
if let Some(arg) = args.first() {
match find_shell_keyword(&checker.ctx, keywords) {
match find_shell_keyword(checker.semantic_model(), keywords) {
// S602
Some(ShellKeyword {
truthiness: Truthiness::Truthy,
keyword,
}) => {
if checker
.settings
.rules
.enabled(Rule::SubprocessPopenWithShellEqualsTrue)
{
if checker.enabled(Rule::SubprocessPopenWithShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(
SubprocessPopenWithShellEqualsTrue {
seems_safe: shell_call_seems_safe(arg),
@@ -209,11 +208,7 @@ pub(crate) fn shell_injection(
truthiness: Truthiness::Falsey | Truthiness::Unknown,
keyword,
}) => {
if checker
.settings
.rules
.enabled(Rule::SubprocessWithoutShellEqualsTrue)
{
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(
SubprocessWithoutShellEqualsTrue,
keyword.range(),
@@ -222,11 +217,7 @@ pub(crate) fn shell_injection(
}
// S603
None => {
if checker
.settings
.rules
.enabled(Rule::SubprocessWithoutShellEqualsTrue)
{
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(
SubprocessWithoutShellEqualsTrue,
arg.range(),
@@ -238,14 +229,10 @@ pub(crate) fn shell_injection(
} else if let Some(ShellKeyword {
truthiness: Truthiness::Truthy,
keyword,
}) = find_shell_keyword(&checker.ctx, keywords)
}) = find_shell_keyword(checker.semantic_model(), keywords)
{
// S604
if checker
.settings
.rules
.enabled(Rule::CallWithShellEqualsTrue)
{
if checker.enabled(Rule::CallWithShellEqualsTrue) {
checker
.diagnostics
.push(Diagnostic::new(CallWithShellEqualsTrue, keyword.range()));
@@ -255,7 +242,7 @@ pub(crate) fn shell_injection(
// S605
if matches!(call_kind, Some(CallKind::Shell)) {
if let Some(arg) = args.first() {
if checker.settings.rules.enabled(Rule::StartProcessWithAShell) {
if checker.enabled(Rule::StartProcessWithAShell) {
checker.diagnostics.push(Diagnostic::new(
StartProcessWithAShell {
seems_safe: shell_call_seems_safe(arg),
@@ -268,11 +255,7 @@ pub(crate) fn shell_injection(
// S606
if matches!(call_kind, Some(CallKind::NoShell)) {
if checker
.settings
.rules
.enabled(Rule::StartProcessWithNoShell)
{
if checker.enabled(Rule::StartProcessWithNoShell) {
checker
.diagnostics
.push(Diagnostic::new(StartProcessWithNoShell, func.range()));
@@ -282,11 +265,7 @@ pub(crate) fn shell_injection(
// S607
if call_kind.is_some() {
if let Some(arg) = args.first() {
if checker
.settings
.rules
.enabled(Rule::StartProcessWithPartialPath)
{
if checker.enabled(Rule::StartProcessWithPartialPath) {
if let Some(value) = try_string_literal(arg) {
if FULL_PATH_REGEX.find(value).is_none() {
checker

View File

@@ -25,7 +25,7 @@ pub(crate) fn snmp_insecure_version(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["pysnmp", "hlapi", "CommunityData"]

View File

@@ -27,7 +27,7 @@ pub(crate) fn snmp_weak_cryptography(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["pysnmp", "hlapi", "UsmUserData"]

View File

@@ -470,7 +470,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
return;
};
let Some(reason) = checker.ctx.resolve_call_path(func).and_then(|call_path| {
let Some(reason) = checker.semantic_model().resolve_call_path(func).and_then(|call_path| {
for module in SUSPICIOUS_MEMBERS {
for member in module.members {
if call_path.as_slice() == *member {
@@ -512,7 +512,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
Reason::FTPLib => SuspiciousFTPLibUsage.into(),
};
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, expr.range());
if checker.settings.rules.enabled(diagnostic.kind.rule()) {
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -27,7 +27,7 @@ pub(crate) fn try_except_continue(
) {
if body.len() == 1
&& body[0].is_continue_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker))
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic_model()))
{
checker
.diagnostics

View File

@@ -27,7 +27,7 @@ pub(crate) fn try_except_pass(
) {
if body.len() == 1
&& body[0].is_pass_stmt()
&& (check_typed_exception || is_untyped_exception(type_, checker))
&& (check_typed_exception || is_untyped_exception(type_, checker.semantic_model()))
{
checker
.diagnostics

View File

@@ -38,14 +38,14 @@ pub(crate) fn unsafe_yaml_load(
keywords: &[Keyword],
) {
if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| call_path.as_slice() == ["yaml", "load"])
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(loader_arg) = call_args.argument("Loader", 1) {
if !checker
.ctx
.semantic_model()
.resolve_call_path(loader_arg)
.map_or(false, |call_path| {
call_path.as_slice() == ["yaml", "SafeLoader"]

View File

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

View File

@@ -34,7 +34,7 @@ pub(crate) fn blind_except(
return;
};
for exception in ["BaseException", "Exception"] {
if id == exception && checker.ctx.is_builtin(exception) {
if id == exception && checker.semantic_model().is_builtin(exception) {
// If the exception is re-raised, don't flag an error.
if body.iter().any(|stmt| {
if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt {
@@ -58,7 +58,7 @@ pub(crate) fn blind_except(
if body.iter().any(|stmt| {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() {
if logging::is_logger_candidate(&checker.ctx, func) {
if logging::is_logger_candidate(func, checker.semantic_model()) {
if let Some(attribute) = func.as_attribute_expr() {
let attr = attribute.attr.as_str();
if attr == "exception" {

View File

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

View File

@@ -7,6 +7,64 @@ use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for boolean positional arguments in function definitions.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller, and to future
/// readers of the code.
///
/// The use of a boolean will also limit the function to only two possible
/// behaviors, which makes the function difficult to extend in the future.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, True) # What does `True` mean?
/// round_number(1.5, False) # What does `False` mean?
/// ```
///
/// Instead, refactor into separate implementations:
/// ```python
/// from math import ceil, floor
///
///
/// def round_up(number: float) -> int:
/// return ceil(number)
///
///
/// def round_down(number: float) -> int:
/// return floor(number)
///
///
/// round_up(1.5)
/// round_down(1.5)
/// ```
///
/// Or, refactor to use an `Enum`:
/// ```python
/// from enum import Enum
///
///
/// class RoundingMethod(Enum):
/// UP = 1
/// DOWN = 2
///
///
/// def round_number(value: float, method: RoundingMethod) -> float:
/// ...
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalArgInFunctionDefinition;
@@ -17,6 +75,44 @@ impl Violation for BooleanPositionalArgInFunctionDefinition {
}
}
/// ## What it does
/// Checks for the use of booleans as default values in function definitions.
///
/// ## Why is this bad?
/// Calling a function with boolean default means that the keyword argument
/// argument can be omitted, which makes the function call ambiguous.
///
/// Instead, define the relevant argument as keyword-only.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool = True) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5)
/// round_number(1.5, up=False)
/// ```
///
/// Use instead:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanDefaultValueInFunctionDefinition;
@@ -27,6 +123,35 @@ impl Violation for BooleanDefaultValueInFunctionDefinition {
}
}
/// ## What it does
/// Checks for boolean positional arguments in function calls.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller, and to future
/// readers of the code.
///
/// ## Example
/// ```python
/// def foo(flag: bool) -> None:
/// ...
///
///
/// foo(True)
/// ```
///
/// Use instead:
/// ```python
/// def foo(flag: bool) -> None:
/// ...
///
///
/// foo(flag=True)
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalValueInFunctionCall;

View File

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

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
use ruff_python_semantic::context::Context;
use ruff_python_semantic::model::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -35,16 +35,16 @@ impl Violation for EmptyMethodWithoutAbstractDecorator {
}
}
fn is_abc_class(context: &Context, bases: &[Expr], keywords: &[Keyword]) -> bool {
fn is_abc_class(model: &SemanticModel, bases: &[Expr], keywords: &[Keyword]) -> bool {
keywords.iter().any(|keyword| {
keyword.arg.as_ref().map_or(false, |arg| arg == "metaclass")
&& context
&& model
.resolve_call_path(&keyword.value)
.map_or(false, |call_path| {
call_path.as_slice() == ["abc", "ABCMeta"]
})
}) || bases.iter().any(|base| {
context
model
.resolve_call_path(base)
.map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"])
})
@@ -79,7 +79,7 @@ pub(crate) fn abstract_base_class(
if bases.len() + keywords.len() != 1 {
return;
}
if !is_abc_class(&checker.ctx, bases, keywords) {
if !is_abc_class(checker.semantic_model(), bases, keywords) {
return;
}
@@ -108,20 +108,16 @@ pub(crate) fn abstract_base_class(
continue;
};
let has_abstract_decorator = is_abstract(&checker.ctx, decorator_list);
let has_abstract_decorator = is_abstract(checker.semantic_model(), decorator_list);
has_abstract_method |= has_abstract_decorator;
if !checker
.settings
.rules
.enabled(Rule::EmptyMethodWithoutAbstractDecorator)
{
if !checker.enabled(Rule::EmptyMethodWithoutAbstractDecorator) {
continue;
}
if !has_abstract_decorator
&& is_empty_body(body)
&& !is_overload(&checker.ctx, decorator_list)
&& !is_overload(checker.semantic_model(), decorator_list)
{
checker.diagnostics.push(Diagnostic::new(
EmptyMethodWithoutAbstractDecorator {
@@ -131,11 +127,7 @@ pub(crate) fn abstract_base_class(
));
}
}
if checker
.settings
.rules
.enabled(Rule::AbstractBaseClassWithoutAbstractMethod)
{
if checker.enabled(Rule::AbstractBaseClassWithoutAbstractMethod) {
if !has_abstract_method {
checker.diagnostics.push(Diagnostic::new(
AbstractBaseClassWithoutAbstractMethod {

View File

@@ -54,7 +54,7 @@ pub(crate) fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg:
let mut diagnostic = Diagnostic::new(AssertFalse, test.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().stmt(&assertion_error(msg)),
stmt.range(),
)));

View File

@@ -66,7 +66,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
}
if !checker
.ctx
.semantic_model()
.resolve_call_path(args.first().unwrap())
.map_or(false, |call_path| call_path.as_slice() == ["", "Exception"])
{
@@ -78,7 +78,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
{
AssertionKind::AssertRaises
} else if checker
.ctx
.semantic_model()
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["pytest", "raises"]

View File

@@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::model::SemanticModel;
use ruff_python_semantic::scope::ScopeKind;
use crate::checkers::ast::Checker;
@@ -18,19 +19,16 @@ impl Violation for CachedInstanceMethod {
}
}
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
checker
.ctx
.resolve_call_path(expr)
.map_or(false, |call_path| {
call_path.as_slice() == ["functools", "lru_cache"]
|| call_path.as_slice() == ["functools", "cache"]
})
fn is_cache_func(model: &SemanticModel, expr: &Expr) -> bool {
model.resolve_call_path(expr).map_or(false, |call_path| {
call_path.as_slice() == ["functools", "lru_cache"]
|| call_path.as_slice() == ["functools", "cache"]
})
}
/// B019
pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) {
if !matches!(checker.ctx.scope().kind, ScopeKind::Class(_)) {
if !matches!(checker.semantic_model().scope().kind, ScopeKind::Class(_)) {
return;
}
for decorator in decorator_list {
@@ -44,7 +42,7 @@ pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Ex
}
for decorator in decorator_list {
if is_cache_func(
checker,
checker.semantic_model(),
match decorator {
Expr::Call(ast::ExprCall { func, .. }) => func,
_ => decorator,

View File

@@ -75,11 +75,7 @@ fn duplicate_handler_exceptions<'a>(
}
}
if checker
.settings
.rules
.enabled(Rule::DuplicateHandlerException)
{
if checker.enabled(Rule::DuplicateHandlerException) {
// TODO(charlie): Handle "BaseException" and redundant exception aliases.
if !duplicates.is_empty() {
let mut diagnostic = Diagnostic::new(
@@ -94,7 +90,7 @@ fn duplicate_handler_exceptions<'a>(
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
if unique_elts.len() == 1 {
checker.generator().expr(unique_elts[0])
} else {
@@ -140,11 +136,7 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthand
}
}
if checker
.settings
.rules
.enabled(Rule::DuplicateTryBlockException)
{
if checker.enabled(Rule::DuplicateTryBlockException) {
for (name, exprs) in duplicates {
for expr in exprs {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -4,11 +4,11 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Ranged};
use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::call_path::{compose_call_path, CallPath};
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::analyze::typing::is_immutable_func;
use ruff_python_semantic::model::SemanticModel;
use crate::checkers::ast::Checker;
use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func;
@@ -73,9 +73,19 @@ impl Violation for FunctionCallInDefaultArgument {
}
struct ArgumentDefaultVisitor<'a> {
checker: &'a Checker<'a>,
diagnostics: Vec<(DiagnosticKind, TextRange)>,
model: &'a SemanticModel<'a>,
extend_immutable_calls: Vec<CallPath<'a>>,
diagnostics: Vec<(DiagnosticKind, TextRange)>,
}
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
model,
extend_immutable_calls,
diagnostics: Vec::new(),
}
}
}
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
@@ -85,8 +95,8 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
match expr {
Expr::Call(ast::ExprCall { func, args, .. }) => {
if !is_mutable_func(self.checker, func)
&& !is_immutable_func(&self.checker.ctx, func, &self.extend_immutable_calls)
if !is_mutable_func(self.model, func)
&& !is_immutable_func(self.model, func, &self.extend_immutable_calls)
&& !is_nan_or_infinity(func, args)
{
self.diagnostics.push((
@@ -139,11 +149,8 @@ pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: &
.map(|target| from_qualified_name(target))
.collect();
let diagnostics = {
let mut visitor = ArgumentDefaultVisitor {
checker,
diagnostics: vec![],
extend_immutable_calls,
};
let mut visitor =
ArgumentDefaultVisitor::new(checker.semantic_model(), extend_immutable_calls);
for expr in arguments
.defaults
.iter()

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use crate::checkers::ast::Checker;

View File

@@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_immutable_annotation;
use ruff_python_semantic::model::SemanticModel;
use crate::checkers::ast::Checker;
@@ -25,18 +26,15 @@ const MUTABLE_FUNCS: &[&[&str]] = &[
&["collections", "deque"],
];
pub(crate) fn is_mutable_func(checker: &Checker, func: &Expr) -> bool {
checker
.ctx
.resolve_call_path(func)
.map_or(false, |call_path| {
MUTABLE_FUNCS
.iter()
.any(|target| call_path.as_slice() == *target)
})
pub(crate) fn is_mutable_func(model: &SemanticModel, func: &Expr) -> bool {
model.resolve_call_path(func).map_or(false, |call_path| {
MUTABLE_FUNCS
.iter()
.any(|target| call_path.as_slice() == *target)
})
}
fn is_mutable_expr(checker: &Checker, expr: &Expr) -> bool {
fn is_mutable_expr(model: &SemanticModel, expr: &Expr) -> bool {
match expr {
Expr::List(_)
| Expr::Dict(_)
@@ -44,7 +42,7 @@ fn is_mutable_expr(checker: &Checker, expr: &Expr) -> bool {
| Expr::ListComp(_)
| Expr::DictComp(_)
| Expr::SetComp(_) => true,
Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(checker, func),
Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(model, func),
_ => false,
}
}
@@ -66,11 +64,10 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume
.zip(arguments.defaults.iter().rev()),
)
{
if is_mutable_expr(checker, default)
&& !arg
.annotation
.as_ref()
.map_or(false, |expr| is_immutable_annotation(&checker.ctx, expr))
if is_mutable_expr(checker.semantic_model(), default)
&& !arg.annotation.as_ref().map_or(false, |expr| {
is_immutable_annotation(checker.semantic_model(), expr)
})
{
checker
.diagnostics

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