Compare commits
44 Commits
charlie/di
...
charlie/sm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b3488b5b2 | ||
|
|
17d938f078 | ||
|
|
5cedf0f724 | ||
|
|
38297c08b4 | ||
|
|
30e90838d0 | ||
|
|
040fb9cef4 | ||
|
|
8961d8eb6f | ||
|
|
31bddef98f | ||
|
|
a59d252246 | ||
|
|
5b9d4f18ae | ||
|
|
c6a760e298 | ||
|
|
3644695bf2 | ||
|
|
b1d01b1950 | ||
|
|
4e84e8a8e2 | ||
|
|
a256fdb9f4 | ||
|
|
7479dfd815 | ||
|
|
ba4c0a21fa | ||
|
|
73e179ffab | ||
|
|
3cbaaa4795 | ||
|
|
2681c0e633 | ||
|
|
f3bdd2e7be | ||
|
|
652c644c2a | ||
|
|
04d273bcc7 | ||
|
|
154439728a | ||
|
|
1ddc577204 | ||
|
|
74effb40b9 | ||
|
|
6c3724ab98 | ||
|
|
3b8121379d | ||
|
|
5ba47c3302 | ||
|
|
b613460fe5 | ||
|
|
daadd24bde | ||
|
|
9308e939f4 | ||
|
|
04c9348de0 | ||
|
|
2d3766d928 | ||
|
|
550b643e33 | ||
|
|
cbe344f4d5 | ||
|
|
063431cb0f | ||
|
|
c6e5fed658 | ||
|
|
f73b398776 | ||
|
|
55c4020ba9 | ||
|
|
d70f899f71 | ||
|
|
19c4b7bee6 | ||
|
|
3238743a7b | ||
|
|
f22c269ccf |
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:
|
||||
|
||||
- Does this pull request include a summary of the change? (See below.)
|
||||
- Does this pull request include a descriptive title?
|
||||
- Does this pull request include references to any relevant issues?
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- What's the purpose of the change? What does it do, and why? -->
|
||||
|
||||
## Test Plan
|
||||
|
||||
<!-- How was it tested? -->
|
||||
24
.github/workflows/release.yaml
vendored
24
.github/workflows/release.yaml
vendored
@@ -3,7 +3,7 @@ name: "[ruff] Release"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
types: [ published ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -406,9 +406,6 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade twine
|
||||
twine upload --skip-existing *
|
||||
- name: "Update pre-commit mirror"
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "pypi_release"}'
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: binaries
|
||||
@@ -417,3 +414,22 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: binaries/*
|
||||
|
||||
# After the release has been published, we update downstream repositories
|
||||
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
|
||||
update-dependents:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: "Update pre-commit mirror"
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'astral-sh',
|
||||
repo: 'ruff-pre-commit',
|
||||
workflow_id: 'main.yml',
|
||||
ref: 'main',
|
||||
})
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,10 +1,10 @@
|
||||
# Local cache
|
||||
.ruff_cache
|
||||
crates/ruff/resources/test/cpython
|
||||
mkdocs.yml
|
||||
.overrides
|
||||
ruff-old
|
||||
github_search*.jsonl
|
||||
schemastore
|
||||
.venv*
|
||||
|
||||
###
|
||||
# Rust.gitignore
|
||||
|
||||
@@ -21,6 +21,7 @@ repos:
|
||||
- --disable
|
||||
- MD013 # line-length
|
||||
- MD033 # no-inline-html
|
||||
- MD041 # first-line-h1
|
||||
- --
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
|
||||
23
Cargo.lock
generated
23
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -86,9 +86,16 @@ while x > 0:
|
||||
):
|
||||
print("Bad module!")
|
||||
|
||||
# SIM102
|
||||
if node.module:
|
||||
if node.module == "multiprocessing" or node.module.startswith(
|
||||
# SIM102 (auto-fixable)
|
||||
if node.module012345678:
|
||||
if node.module == "multiprocß9💣2ℝ" or node.module.startswith(
|
||||
"multiprocessing."
|
||||
):
|
||||
print("Bad module!")
|
||||
|
||||
# SIM102 (not auto-fixable)
|
||||
if node.module0123456789:
|
||||
if node.module == "multiprocß9💣2ℝ" or node.module.startswith(
|
||||
"multiprocessing."
|
||||
):
|
||||
print("Bad module!")
|
||||
|
||||
@@ -80,17 +80,25 @@ else:
|
||||
|
||||
# SIM108
|
||||
if a:
|
||||
b = cccccccccccccccccccccccccccccccccccc
|
||||
b = "cccccccccccccccccccccccccccccccccß"
|
||||
else:
|
||||
b = ddddddddddddddddddddddddddddddddddddd
|
||||
b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
|
||||
|
||||
# OK (too long)
|
||||
if True:
|
||||
if a:
|
||||
b = cccccccccccccccccccccccccccccccccccc
|
||||
b = ccccccccccccccccccccccccccccccccccc
|
||||
else:
|
||||
b = ddddddddddddddddddddddddddddddddddddd
|
||||
b = ddddddddddddddddddddddddddddddddddd
|
||||
|
||||
|
||||
# OK (too long with tabs)
|
||||
if True:
|
||||
if a:
|
||||
b = ccccccccccccccccccccccccccccccccccc
|
||||
else:
|
||||
b = ddddddddddddddddddddddddddddddddddd
|
||||
|
||||
|
||||
# SIM108 (without fix due to trailing comment)
|
||||
|
||||
@@ -155,3 +155,19 @@ def f():
|
||||
if check(x):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ":
|
||||
if x.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
# OK (too long)
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9":
|
||||
if x.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -171,3 +171,19 @@ def f():
|
||||
if x > y:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9":
|
||||
if x.isdigit():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# OK (too long)
|
||||
for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß90":
|
||||
if x.isdigit():
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -90,3 +90,13 @@ with (
|
||||
D() as d,
|
||||
):
|
||||
print("hello")
|
||||
|
||||
# SIM117 (auto-fixable)
|
||||
with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
print("hello")
|
||||
|
||||
# SIM117 (not auto-fixable too long)
|
||||
with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a:
|
||||
with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
print("hello")
|
||||
@@ -36,12 +36,18 @@ else:
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
|
||||
###
|
||||
# Negative cases
|
||||
###
|
||||
|
||||
# OK (too long)
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß"
|
||||
|
||||
# OK (false negative)
|
||||
if not key in a_dict:
|
||||
var = "default"
|
||||
|
||||
@@ -146,3 +146,7 @@ def f():
|
||||
import pandas as pd
|
||||
|
||||
x = dict[pd.DataFrame, pd.DataFrame]
|
||||
|
||||
|
||||
def f():
|
||||
import pandas as pd
|
||||
|
||||
@@ -2,3 +2,7 @@ import a
|
||||
# Don't take this comment into account when determining whether the next import can fit on one line.
|
||||
from b import c
|
||||
from d import e # Do take this comment into account when determining whether the next import can fit on one line.
|
||||
# The next import fits on one line.
|
||||
from f import g # 012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ
|
||||
# The next import doesn't fit on one line.
|
||||
from h import i # 012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9
|
||||
|
||||
4
crates/ruff/resources/test/fixtures/isort/required_imports/off.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/isort/required_imports/off.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# isort: off
|
||||
|
||||
x = 1
|
||||
# isort: on
|
||||
@@ -6,7 +6,16 @@ import f
|
||||
import c
|
||||
import d
|
||||
|
||||
# isort: split
|
||||
# isort: split
|
||||
|
||||
import a
|
||||
import b
|
||||
|
||||
if True:
|
||||
import C
|
||||
import A
|
||||
|
||||
# isort: split
|
||||
|
||||
import D
|
||||
import B
|
||||
|
||||
11
crates/ruff/resources/test/fixtures/pycodestyle/E501_2.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/pycodestyle/E501_2.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
31
crates/ruff/resources/test/fixtures/pycodestyle/W505_utf_8.py
vendored
Normal file
31
crates/ruff/resources/test/fixtures/pycodestyle/W505_utf_8.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Here's a top-level ß9💣2ℝing that's over theß9💣2ℝ."""
|
||||
|
||||
|
||||
def f1():
|
||||
"""Here's a ß9💣2ℝing that's also over theß9💣2ℝ."""
|
||||
|
||||
x = 1 # Here's a comment that's over theß9💣2ℝ, but it's not standalone.
|
||||
|
||||
# Here's a standalone comment that's over theß9💣2ℝ.
|
||||
|
||||
x = 2
|
||||
# Another standalone that is preceded by a newline and indent toke and is over theß9💣2ℝ.
|
||||
|
||||
print("Here's a string that's over theß9💣2ℝ, but it's not a ß9💣2ℝing.")
|
||||
|
||||
|
||||
"This is also considered a ß9💣2ℝing, and is over theß9💣2ℝ."
|
||||
|
||||
|
||||
def f2():
|
||||
"""Here's a multi-line ß9💣2ℝing.
|
||||
|
||||
It's over theß9💣2ℝ on this line, which isn't the first line in the ß9💣2ℝing.
|
||||
"""
|
||||
|
||||
|
||||
def f3():
|
||||
"""Here's a multi-line ß9💣2ℝing.
|
||||
|
||||
It's over theß9💣2ℝ on this line, which isn't the first line in the ß9💣2ℝing."""
|
||||
4
crates/ruff/resources/test/fixtures/pyflakes/F811_23.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/pyflakes/F811_23.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Test that shadowing an explicit re-export produces a warning."""
|
||||
|
||||
import foo as foo
|
||||
import bar as foo
|
||||
5
crates/ruff/resources/test/fixtures/pyflakes/F811_24.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pyflakes/F811_24.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test that shadowing a `__future__` import does not produce a warning."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import annotations
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,24 @@ def f():
|
||||
...
|
||||
|
||||
|
||||
def g():
|
||||
def f():
|
||||
"""Here's a docstring with a greek rho: ρ"""
|
||||
# And here's a comment with a greek alpha: ∗
|
||||
...
|
||||
|
||||
|
||||
x = "𝐁ad string"
|
||||
x = "−"
|
||||
|
||||
# This should be ignored, since it contains an unambiguous unicode character, and no
|
||||
# ASCII.
|
||||
x = "Русский"
|
||||
|
||||
# The first word should be ignored, while the second should be included, since it
|
||||
# contains ASCII.
|
||||
x = "βα Bαd"
|
||||
|
||||
# The two characters should be flagged here. The first character is a "word"
|
||||
# consisting of a single ambiguous character, while the second character is a "word
|
||||
# boundary" (whitespace) that it itself ambiguous.
|
||||
x = "Р усский"
|
||||
|
||||
23
crates/ruff/resources/test/fixtures/ruff/noqa.py
vendored
Normal file
23
crates/ruff/resources/test/fixtures/ruff/noqa.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
def f():
|
||||
# These should both be ignored by the `noqa`.
|
||||
I = 1 # noqa: E741, F841
|
||||
|
||||
|
||||
def f():
|
||||
# These should both be ignored by the `noqa`.
|
||||
I = 1 # noqa: E741,F841
|
||||
|
||||
|
||||
def f():
|
||||
# These should both be ignored by the `noqa`.
|
||||
I = 1 # noqa: E741 F841
|
||||
|
||||
|
||||
def f():
|
||||
# These should both be ignored by the `noqa`.
|
||||
I = 1 # noqa: E741 , F841
|
||||
|
||||
|
||||
def f():
|
||||
# Only `E741` should be ignored by the `noqa`.
|
||||
I = 1 # noqa: E741.F841
|
||||
@@ -10,14 +10,11 @@ use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers;
|
||||
use ruff_python_ast::imports::{AnyImport, Import};
|
||||
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_semantic::context::Context;
|
||||
|
||||
use crate::cst::helpers::compose_module_path;
|
||||
use crate::cst::matchers::match_module;
|
||||
use crate::importer::Importer;
|
||||
use crate::cst::matchers::match_statement;
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account
|
||||
/// deleted.
|
||||
@@ -215,9 +212,9 @@ pub(crate) fn remove_unused_imports<'a>(
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(stmt.range());
|
||||
let mut tree = match_module(module_text)?;
|
||||
let mut tree = match_statement(module_text)?;
|
||||
|
||||
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
|
||||
let Statement::Simple(body) = &mut tree else {
|
||||
bail!("Expected Statement::Simple");
|
||||
};
|
||||
|
||||
@@ -423,86 +420,6 @@ pub(crate) fn remove_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make the
|
||||
/// symbol available in the current scope along with the bound name of the symbol.
|
||||
///
|
||||
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
|
||||
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with the
|
||||
/// name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
|
||||
///
|
||||
/// Attempts to reuse existing imports when possible.
|
||||
pub(crate) fn get_or_import_symbol(
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
context: &Context,
|
||||
importer: &Importer,
|
||||
locator: &Locator,
|
||||
) -> Result<(Edit, String)> {
|
||||
if let Some((source, binding)) = context.resolve_qualified_import_name(module, member) {
|
||||
// If the symbol is already available in the current scope, use it.
|
||||
|
||||
// The exception: the symbol source (i.e., the import statement) comes after the current
|
||||
// location. For example, we could be generating an edit within a function, and the import
|
||||
// could be defined in the module scope, but after the function definition. In this case,
|
||||
// it's unclear whether we can use the symbol (the function could be called between the
|
||||
// import and the current location, and thus the symbol would not be available). It's also
|
||||
// unclear whether should add an import statement at the top of the file, since it could
|
||||
// be shadowed between the import and the current location.
|
||||
if source.start() > at {
|
||||
bail!("Unable to use existing symbol `{binding}` due to late-import");
|
||||
}
|
||||
|
||||
// We also add a no-op edit to force conflicts with any other fixes that might try to
|
||||
// remove the import. Consider:
|
||||
//
|
||||
// ```py
|
||||
// import sys
|
||||
//
|
||||
// quit()
|
||||
// ```
|
||||
//
|
||||
// Assume you omit this no-op edit. If you run Ruff with `unused-imports` and
|
||||
// `sys-exit-alias` over this snippet, it will generate two fixes: (1) remove the unused
|
||||
// `sys` import; and (2) replace `quit()` with `sys.exit()`, under the assumption that `sys`
|
||||
// is already imported and available.
|
||||
//
|
||||
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
|
||||
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
|
||||
let import_edit =
|
||||
Edit::range_replacement(locator.slice(source.range()).to_string(), source.range());
|
||||
Ok((import_edit, binding))
|
||||
} else {
|
||||
if let Some(stmt) = importer.find_import_from(module, at) {
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
if context
|
||||
.find_binding(member)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit = importer.add_member(stmt, member)?;
|
||||
Ok((import_edit, member.to_string()))
|
||||
} else {
|
||||
bail!("Unable to insert `{member}` into scope due to name conflict")
|
||||
}
|
||||
} else {
|
||||
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
|
||||
// return `"functools.cache"` as the bound name.
|
||||
if context
|
||||
.find_binding(module)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit =
|
||||
importer.add_import(&AnyImport::Import(Import::module(module)), at);
|
||||
Ok((import_edit, format!("{module}.{member}")))
|
||||
} else {
|
||||
bail!("Unable to insert `{module}` into scope due to name conflict")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_python_semantic::context::Snapshot;
|
||||
use ruff_python_semantic::model::Snapshot;
|
||||
|
||||
/// A collection of AST nodes that are deferred for later analysis.
|
||||
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ use ruff_python_stdlib::path::is_python_stub_file;
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::isort;
|
||||
use crate::rules::isort::track::{Block, ImportTracker};
|
||||
use crate::rules::isort::block::{Block, BlockBuilder};
|
||||
use crate::settings::Settings;
|
||||
|
||||
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
|
||||
@@ -86,9 +86,9 @@ pub(crate) fn check_imports(
|
||||
) -> (Vec<Diagnostic>, Option<ImportMap>) {
|
||||
let is_stub = is_python_stub_file(path);
|
||||
|
||||
// Extract all imports from the AST.
|
||||
// Extract all import blocks from the AST.
|
||||
let tracker = {
|
||||
let mut tracker = ImportTracker::new(locator, directives, is_stub);
|
||||
let mut tracker = BlockBuilder::new(locator, directives, is_stub);
|
||||
tracker.visit_body(python_ast);
|
||||
tracker
|
||||
};
|
||||
@@ -109,7 +109,7 @@ pub(crate) fn check_imports(
|
||||
}
|
||||
if settings.rules.enabled(Rule::MissingRequiredImport) {
|
||||
diagnostics.extend(isort::rules::add_required_imports(
|
||||
&blocks, python_ast, locator, stylist, settings, is_stub,
|
||||
python_ast, locator, stylist, settings, is_stub,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ mod tests {
|
||||
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
|
||||
use crate::line_width::LineLength;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
|
||||
@@ -196,7 +197,7 @@ mod tests {
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
let check_with_max_line_length = |line_length: LineLength| {
|
||||
check_physical_lines(
|
||||
Path::new("foo.py"),
|
||||
&locator,
|
||||
@@ -209,7 +210,8 @@ mod tests {
|
||||
},
|
||||
)
|
||||
};
|
||||
assert_eq!(check_with_max_line_length(8), vec![]);
|
||||
assert_eq!(check_with_max_line_length(8), vec![]);
|
||||
let line_length = LineLength::from(8);
|
||||
assert_eq!(check_with_max_line_length(line_length), vec![]);
|
||||
assert_eq!(check_with_max_line_length(line_length), vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ use crate::rules::{
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator};
|
||||
|
||||
pub(crate) fn check_tokens(
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
is_stub: bool,
|
||||
@@ -100,15 +101,9 @@ pub(crate) fn check_tokens(
|
||||
|
||||
// ERA001
|
||||
if enforce_commented_out_code {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if matches!(tok, Tok::Comment(_)) {
|
||||
if let Some(diagnostic) =
|
||||
eradicate::rules::commented_out_code(locator, *range, settings)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics.extend(eradicate::rules::commented_out_code(
|
||||
indexer, locator, settings,
|
||||
));
|
||||
}
|
||||
|
||||
// W605
|
||||
@@ -185,13 +180,13 @@ pub(crate) fn check_tokens(
|
||||
|
||||
// PYI033
|
||||
if enforce_type_comment_in_stub && is_stub {
|
||||
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(tokens));
|
||||
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(indexer, locator));
|
||||
}
|
||||
|
||||
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
|
||||
if enforce_todos {
|
||||
diagnostics.extend(
|
||||
flake8_todos::rules::todos(tokens, settings)
|
||||
flake8_todos::rules::todos(indexer, locator, settings)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +83,7 @@ pub fn extract_directives(
|
||||
}
|
||||
|
||||
/// Extract a mapping from logical line to noqa line.
|
||||
pub fn extract_noqa_line_for(
|
||||
lxr: &[LexResult],
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) -> NoqaMapping {
|
||||
fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer) -> NoqaMapping {
|
||||
let mut string_mappings = Vec::new();
|
||||
|
||||
for (tok, range) in lxr.iter().flatten() {
|
||||
@@ -166,7 +162,7 @@ pub fn extract_noqa_line_for(
|
||||
}
|
||||
|
||||
/// Extract a set of ranges over which to disable isort.
|
||||
pub fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
|
||||
fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
|
||||
let mut exclusions: Vec<TextRange> = Vec::default();
|
||||
let mut splits: Vec<TextSize> = Vec::default();
|
||||
let mut off: Option<TextSize> = None;
|
||||
|
||||
@@ -57,11 +57,6 @@ impl<'a> DocstringBody<'a> {
|
||||
self.range().start()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end(self) -> TextSize {
|
||||
self.range().end()
|
||||
}
|
||||
|
||||
pub(crate) fn range(self) -> TextRange {
|
||||
self.docstring.body_range + self.docstring.start()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
|
||||
use ruff_python_ast::whitespace;
|
||||
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::{Docstring, DocstringBody};
|
||||
use ruff_python_ast::whitespace;
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum SectionKind {
|
||||
pub(crate) enum SectionKind {
|
||||
Args,
|
||||
Arguments,
|
||||
Attention,
|
||||
@@ -48,7 +50,7 @@ pub enum SectionKind {
|
||||
}
|
||||
|
||||
impl SectionKind {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
pub(crate) fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"args" => Some(Self::Args),
|
||||
"arguments" => Some(Self::Arguments),
|
||||
@@ -89,7 +91,7 @@ impl SectionKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Args => "Args",
|
||||
Self::Arguments => "Arguments",
|
||||
@@ -217,7 +219,7 @@ impl Debug for SectionContexts<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SectionContextsIter<'a> {
|
||||
pub(crate) struct SectionContextsIter<'a> {
|
||||
docstring_body: DocstringBody<'a>,
|
||||
inner: std::slice::Iter<'a, SectionContextData>,
|
||||
}
|
||||
@@ -266,28 +268,24 @@ struct SectionContextData {
|
||||
summary_full_end: TextSize,
|
||||
}
|
||||
|
||||
pub struct SectionContext<'a> {
|
||||
pub(crate) struct SectionContext<'a> {
|
||||
data: &'a SectionContextData,
|
||||
docstring_body: DocstringBody<'a>,
|
||||
}
|
||||
|
||||
impl<'a> SectionContext<'a> {
|
||||
pub fn is_last(&self) -> bool {
|
||||
self.range().end() == self.docstring_body.end()
|
||||
}
|
||||
|
||||
/// The `kind` of the section, e.g. [`SectionKind::Args`] or [`SectionKind::Returns`].
|
||||
pub const fn kind(&self) -> SectionKind {
|
||||
pub(crate) const fn kind(&self) -> SectionKind {
|
||||
self.data.kind
|
||||
}
|
||||
|
||||
/// The name of the section as it appears in the docstring, e.g. "Args" or "Returns".
|
||||
pub fn section_name(&self) -> &'a str {
|
||||
pub(crate) fn section_name(&self) -> &'a str {
|
||||
&self.docstring_body.as_str()[self.data.name_range]
|
||||
}
|
||||
|
||||
/// Returns the rest of the summary line after the section name.
|
||||
pub fn summary_after_section_name(&self) -> &'a str {
|
||||
pub(crate) fn summary_after_section_name(&self) -> &'a str {
|
||||
&self.summary_line()[usize::from(self.data.name_range.end() - self.data.range.start())..]
|
||||
}
|
||||
|
||||
@@ -296,17 +294,12 @@ impl<'a> SectionContext<'a> {
|
||||
}
|
||||
|
||||
/// The absolute range of the section name
|
||||
pub fn section_name_range(&self) -> TextRange {
|
||||
pub(crate) fn section_name_range(&self) -> TextRange {
|
||||
self.data.name_range + self.offset()
|
||||
}
|
||||
|
||||
/// Summary range relative to the start of the document. Includes the trailing newline.
|
||||
pub fn summary_full_range(&self) -> TextRange {
|
||||
self.summary_full_range_relative() + self.offset()
|
||||
}
|
||||
|
||||
/// The absolute range of the summary line, excluding any trailing newline character.
|
||||
pub fn summary_range(&self) -> TextRange {
|
||||
pub(crate) fn summary_range(&self) -> TextRange {
|
||||
TextRange::at(self.range().start(), self.summary_line().text_len())
|
||||
}
|
||||
|
||||
@@ -321,12 +314,12 @@ impl<'a> SectionContext<'a> {
|
||||
}
|
||||
|
||||
/// The absolute range of the full-section.
|
||||
pub fn range(&self) -> TextRange {
|
||||
pub(crate) fn range(&self) -> TextRange {
|
||||
self.range_relative() + self.offset()
|
||||
}
|
||||
|
||||
/// Summary line without the trailing newline characters
|
||||
pub fn summary_line(&self) -> &'a str {
|
||||
pub(crate) fn summary_line(&self) -> &'a str {
|
||||
let full_summary = &self.docstring_body.as_str()[self.summary_full_range_relative()];
|
||||
|
||||
let mut bytes = full_summary.bytes().rev();
|
||||
@@ -347,14 +340,14 @@ impl<'a> SectionContext<'a> {
|
||||
}
|
||||
|
||||
/// Returns the text of the last line of the previous section or an empty string if it is the first section.
|
||||
pub fn previous_line(&self) -> Option<&'a str> {
|
||||
pub(crate) fn previous_line(&self) -> Option<&'a str> {
|
||||
let previous =
|
||||
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
|
||||
previous.universal_newlines().last().map(|l| l.as_str())
|
||||
}
|
||||
|
||||
/// Returns the lines belonging to this section after the summary line.
|
||||
pub fn following_lines(&self) -> UniversalNewlineIterator<'a> {
|
||||
pub(crate) fn following_lines(&self) -> UniversalNewlineIterator<'a> {
|
||||
let lines = self.following_lines_str();
|
||||
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
|
||||
}
|
||||
@@ -369,7 +362,7 @@ impl<'a> SectionContext<'a> {
|
||||
}
|
||||
|
||||
/// Returns the absolute range of the following lines.
|
||||
pub fn following_range(&self) -> TextRange {
|
||||
pub(crate) fn following_range(&self) -> TextRange {
|
||||
self.following_range_relative() + self.offset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,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);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
pub use converter::convert;
|
||||
pub use external_config::ExternalConfig;
|
||||
pub use plugin::Plugin;
|
||||
pub use pyproject::parse;
|
||||
|
||||
mod black;
|
||||
mod converter;
|
||||
mod external_config;
|
||||
@@ -6,8 +11,3 @@ mod parser;
|
||||
pub mod pep621;
|
||||
mod plugin;
|
||||
mod pyproject;
|
||||
|
||||
pub use converter::convert;
|
||||
pub use external_config::ExternalConfig;
|
||||
pub use plugin::Plugin;
|
||||
pub use pyproject::parse;
|
||||
|
||||
@@ -195,12 +195,13 @@ pub(crate) fn collect_per_file_ignores(
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
|
||||
use crate::codes;
|
||||
use crate::registry::Linter;
|
||||
use crate::rule_selector::RuleSelector;
|
||||
use crate::settings::types::PatternPrefixPair;
|
||||
|
||||
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
|
||||
|
||||
#[test]
|
||||
fn it_parses_prefix_codes() {
|
||||
let actual = parse_prefix_codes("");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser::ast::{Ranged, Stmt};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
//! Add and modify import statements to make module members available during fix execution.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser::ast::{self, Ranged, Stmt, Suite};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::imports::AnyImport;
|
||||
use ruff_python_ast::imports::{AnyImport, Import};
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_semantic::model::SemanticModel;
|
||||
|
||||
use crate::cst::matchers::{match_aliases, match_import_from, match_module};
|
||||
use crate::cst::matchers::{match_aliases, match_import_from, match_statement};
|
||||
use crate::importer::insertion::Insertion;
|
||||
|
||||
mod insertion;
|
||||
@@ -40,14 +41,6 @@ impl<'a> Importer<'a> {
|
||||
self.ordered_imports.push(import);
|
||||
}
|
||||
|
||||
/// Return the import statement that precedes the given position, if any.
|
||||
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
|
||||
self.ordered_imports
|
||||
.partition_point(|stmt| stmt.start() < at)
|
||||
.checked_sub(1)
|
||||
.map(|idx| self.ordered_imports[idx])
|
||||
}
|
||||
|
||||
/// Add an import statement to import the given module.
|
||||
///
|
||||
/// If there are no existing imports, the new import will be added at the top
|
||||
@@ -66,9 +59,123 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
|
||||
/// the symbol available in the current scope along with the bound name of the symbol.
|
||||
///
|
||||
/// Attempts to reuse existing imports when possible.
|
||||
pub(crate) fn get_or_import_symbol(
|
||||
&self,
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
semantic_model: &SemanticModel,
|
||||
) -> Result<(Edit, String)> {
|
||||
self.get_symbol(module, member, at, semantic_model)?
|
||||
.map_or_else(
|
||||
|| self.import_symbol(module, member, at, semantic_model),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn get_symbol(
|
||||
&self,
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
semantic_model: &SemanticModel,
|
||||
) -> Result<Option<(Edit, String)>> {
|
||||
// If the symbol is already available in the current scope, use it.
|
||||
let Some((source, binding)) = semantic_model.resolve_qualified_import_name(module, member) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// The exception: the symbol source (i.e., the import statement) comes after the current
|
||||
// location. For example, we could be generating an edit within a function, and the import
|
||||
// could be defined in the module scope, but after the function definition. In this case,
|
||||
// it's unclear whether we can use the symbol (the function could be called between the
|
||||
// import and the current location, and thus the symbol would not be available). It's also
|
||||
// unclear whether should add an import statement at the top of the file, since it could
|
||||
// be shadowed between the import and the current location.
|
||||
if source.start() > at {
|
||||
bail!("Unable to use existing symbol `{binding}` due to late-import");
|
||||
}
|
||||
|
||||
// We also add a no-op edit to force conflicts with any other fixes that might try to
|
||||
// remove the import. Consider:
|
||||
//
|
||||
// ```py
|
||||
// import sys
|
||||
//
|
||||
// quit()
|
||||
// ```
|
||||
//
|
||||
// Assume you omit this no-op edit. If you run Ruff with `unused-imports` and
|
||||
// `sys-exit-alias` over this snippet, it will generate two fixes: (1) remove the unused
|
||||
// `sys` import; and (2) replace `quit()` with `sys.exit()`, under the assumption that `sys`
|
||||
// is already imported and available.
|
||||
//
|
||||
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
|
||||
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
|
||||
let import_edit = Edit::range_replacement(
|
||||
self.locator.slice(source.range()).to_string(),
|
||||
source.range(),
|
||||
);
|
||||
Ok(Some((import_edit, binding)))
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
|
||||
/// the symbol available in the current scope along with the bound name of the symbol.
|
||||
///
|
||||
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
|
||||
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with
|
||||
/// the name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
|
||||
fn import_symbol(
|
||||
&self,
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
semantic_model: &SemanticModel,
|
||||
) -> Result<(Edit, String)> {
|
||||
if let Some(stmt) = self.find_import_from(module, at) {
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
if semantic_model
|
||||
.find_binding(member)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit = self.add_member(stmt, member)?;
|
||||
Ok((import_edit, member.to_string()))
|
||||
} else {
|
||||
bail!("Unable to insert `{member}` into scope due to name conflict")
|
||||
}
|
||||
} else {
|
||||
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
|
||||
// return `"functools.cache"` as the bound name.
|
||||
if semantic_model
|
||||
.find_binding(module)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit = self.add_import(&AnyImport::Import(Import::module(module)), at);
|
||||
Ok((import_edit, format!("{module}.{member}")))
|
||||
} else {
|
||||
bail!("Unable to insert `{module}` into scope due to name conflict")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the import statement that precedes the given position, if any.
|
||||
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
|
||||
self.ordered_imports
|
||||
.partition_point(|stmt| stmt.start() < at)
|
||||
.checked_sub(1)
|
||||
.map(|idx| self.ordered_imports[idx])
|
||||
}
|
||||
|
||||
/// Return the top-level [`Stmt`] that imports the given module using `Stmt::ImportFrom`
|
||||
/// preceding the given position, if any.
|
||||
pub(crate) fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
|
||||
fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
|
||||
let mut import_from = None;
|
||||
for stmt in &self.ordered_imports {
|
||||
if stmt.start() >= at {
|
||||
@@ -91,9 +198,9 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
|
||||
/// Add the given member to an existing `Stmt::ImportFrom` statement.
|
||||
pub(crate) fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
|
||||
let mut tree = match_module(self.locator.slice(stmt.range()))?;
|
||||
let import_from = match_import_from(&mut tree)?;
|
||||
fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
|
||||
let mut statement = match_statement(self.locator.slice(stmt.range()))?;
|
||||
let import_from = match_import_from(&mut statement)?;
|
||||
let aliases = match_aliases(import_from)?;
|
||||
aliases.push(ImportAlias {
|
||||
name: NameOrAttribute::N(Box::new(Name {
|
||||
@@ -109,7 +216,7 @@ impl<'a> Importer<'a> {
|
||||
default_indent: self.stylist.indentation(),
|
||||
..CodegenState::default()
|
||||
};
|
||||
tree.codegen(&mut state);
|
||||
statement.codegen(&mut state);
|
||||
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Utils for reading and writing jupyter notebooks
|
||||
|
||||
mod notebook;
|
||||
mod schema;
|
||||
|
||||
pub use notebook::*;
|
||||
pub use schema::*;
|
||||
|
||||
mod notebook;
|
||||
mod schema;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::iter;
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_text_size::TextRange;
|
||||
use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ pub mod fs;
|
||||
mod importer;
|
||||
pub mod jupyter;
|
||||
mod lex;
|
||||
pub mod line_width;
|
||||
pub mod linter;
|
||||
pub mod logging;
|
||||
pub mod message;
|
||||
|
||||
165
crates/ruff/src/line_width.rs
Normal file
165
crates/ruff/src/line_width.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
/// The length of a line of text that is considered too long.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, CacheKey)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct LineLength(usize);
|
||||
|
||||
impl Default for LineLength {
|
||||
/// The default line length.
|
||||
fn default() -> Self {
|
||||
Self(88)
|
||||
}
|
||||
}
|
||||
|
||||
impl LineLength {
|
||||
pub const fn get(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for LineLength {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A measure of the width of a line of text.
|
||||
///
|
||||
/// This is used to determine if a line is too long.
|
||||
/// It should be compared to a [`LineLength`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LineWidth {
|
||||
/// The width of the line.
|
||||
width: usize,
|
||||
/// The column of the line.
|
||||
/// This is used to calculate the width of tabs.
|
||||
column: usize,
|
||||
/// The tab size to use when calculating the width of tabs.
|
||||
tab_size: TabSize,
|
||||
}
|
||||
|
||||
impl Default for LineWidth {
|
||||
fn default() -> Self {
|
||||
Self::new(TabSize::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LineWidth {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.width == other.width
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LineWidth {}
|
||||
|
||||
impl PartialOrd for LineWidth {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.width.partial_cmp(&other.width)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for LineWidth {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.width.cmp(&other.width)
|
||||
}
|
||||
}
|
||||
|
||||
impl LineWidth {
|
||||
pub fn get(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Creates a new `LineWidth` with the given tab size.
|
||||
pub fn new(tab_size: TabSize) -> Self {
|
||||
LineWidth {
|
||||
width: 0,
|
||||
column: 0,
|
||||
tab_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
|
||||
let tab_size: usize = self.tab_size.into();
|
||||
for c in chars {
|
||||
match c {
|
||||
'\t' => {
|
||||
let tab_offset = tab_size - (self.column % tab_size);
|
||||
self.width += tab_offset;
|
||||
self.column += tab_offset;
|
||||
}
|
||||
'\n' | '\r' => {
|
||||
self.width = 0;
|
||||
self.column = 0;
|
||||
}
|
||||
_ => {
|
||||
self.width += c.width().unwrap_or(0);
|
||||
self.column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds the given text to the line width.
|
||||
#[must_use]
|
||||
pub fn add_str(self, text: &str) -> Self {
|
||||
self.update(text.chars())
|
||||
}
|
||||
|
||||
/// Adds the given character to the line width.
|
||||
#[must_use]
|
||||
pub fn add_char(self, c: char) -> Self {
|
||||
self.update(std::iter::once(c))
|
||||
}
|
||||
|
||||
/// Adds the given width to the line width.
|
||||
/// Also adds the given width to the column.
|
||||
/// It is generally better to use [`LineWidth::add_str`] or [`LineWidth::add_char`].
|
||||
/// The width and column should be the same for the corresponding text.
|
||||
/// Currently, this is only used to add spaces.
|
||||
#[must_use]
|
||||
pub fn add_width(mut self, width: usize) -> Self {
|
||||
self.width += width;
|
||||
self.column += width;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<LineLength> for LineWidth {
|
||||
fn eq(&self, other: &LineLength) -> bool {
|
||||
self.width == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<LineLength> for LineWidth {
|
||||
fn partial_cmp(&self, other: &LineLength) -> Option<std::cmp::Ordering> {
|
||||
self.width.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of a tab.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TabSize(pub u8);
|
||||
|
||||
impl Default for TabSize {
|
||||
fn default() -> Self {
|
||||
Self(4)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for TabSize {
|
||||
fn from(tab_size: u8) -> Self {
|
||||
Self(tab_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TabSize> for usize {
|
||||
fn from(tab_size: TabSize) -> Self {
|
||||
tab_size.0 as usize
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_tokens())
|
||||
{
|
||||
let is_stub = is_python_stub_file(path);
|
||||
diagnostics.extend(check_tokens(locator, &tokens, settings, is_stub));
|
||||
diagnostics.extend(check_tokens(locator, indexer, &tokens, settings, is_stub));
|
||||
}
|
||||
|
||||
// Run the filesystem-based rules.
|
||||
|
||||
@@ -2,15 +2,17 @@ use std::fmt::{Display, Formatter, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::fs;
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use fern;
|
||||
use log::Level;
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_python_ast::source_code::SourceCode;
|
||||
use rustpython_parser::{ParseError, ParseErrorType};
|
||||
|
||||
use ruff_python_ast::source_code::SourceCode;
|
||||
|
||||
use crate::fs;
|
||||
|
||||
pub(crate) static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
|
||||
/// Warn a user once, with uniqueness determined by the given ID.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate error logging commands for Azure Pipelines format.
|
||||
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
|
||||
@@ -42,9 +44,10 @@ impl Emitter for AzureEmitter {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::AzureEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use crate::message::Message;
|
||||
use colored::{Color, ColoredString, Colorize, Styles};
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceFile};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use colored::{Color, ColoredString, Colorize, Styles};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceFile};
|
||||
|
||||
use crate::message::Message;
|
||||
|
||||
/// Renders a diff that shows the code fixes.
|
||||
///
|
||||
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate error workflow command in GitHub Actions format.
|
||||
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
|
||||
@@ -57,9 +59,10 @@ impl Emitter for GithubEmitter {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GithubEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use crate::fs::{relativize_path, relativize_path_to};
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
|
||||
use crate::fs::{relativize_path, relativize_path_to};
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
pub struct GitlabEmitter {
|
||||
@@ -122,9 +125,10 @@ fn fingerprint(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GitlabEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_python_ast::source_code::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::JupyterIndex;
|
||||
use crate::message::diff::calculate_print_width;
|
||||
@@ -5,11 +13,6 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{
|
||||
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use ruff_python_ast::source_code::OneIndexed;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GroupedEmitter {
|
||||
@@ -175,9 +178,10 @@ impl std::fmt::Write for PadAdapter<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GroupedEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::source_code::SourceCode;
|
||||
use std::io::Write;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::source_code::SourceCode;
|
||||
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonEmitter;
|
||||
@@ -94,9 +97,10 @@ impl Serialize for ExpandedEdits<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::JsonEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
|
||||
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
|
||||
use crate::message::{
|
||||
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
|
||||
};
|
||||
use crate::registry::AsRule;
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
|
||||
use ruff_python_ast::source_code::SourceLocation;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JunitEmitter;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_python_ast::source_code::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_python_ast::source_code::OneIndexed;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate violations in Pylint format.
|
||||
/// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter)
|
||||
@@ -40,9 +42,10 @@ impl Emitter for PylintEmitter {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::PylintEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
||||
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
|
||||
use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
||||
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::line_width::{LineWidth, TabSize};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
struct EmitterFlags: u8 {
|
||||
/// Whether to show the fix status of a diagnostic.
|
||||
const SHOW_FIX_STATUS = 0b0000_0001;
|
||||
const SHOW_FIX = 0b0000_0010;
|
||||
/// Whether to show the diff of a fix, for diagnostics that have a fix.
|
||||
const SHOW_FIX_DIFF = 0b0000_0010;
|
||||
/// Whether to show the source code of a diagnostic.
|
||||
const SHOW_SOURCE = 0b0000_0100;
|
||||
}
|
||||
}
|
||||
@@ -35,8 +42,8 @@ impl TextEmitter {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_show_fix(mut self, show_fix: bool) -> Self {
|
||||
self.flags.set(EmitterFlags::SHOW_FIX, show_fix);
|
||||
pub fn with_show_fix_diff(mut self, show_fix_diff: bool) -> Self {
|
||||
self.flags.set(EmitterFlags::SHOW_FIX_DIFF, show_fix_diff);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -101,7 +108,7 @@ impl Emitter for TextEmitter {
|
||||
writeln!(writer, "{}", MessageCodeFrame { message })?;
|
||||
}
|
||||
|
||||
if self.flags.contains(EmitterFlags::SHOW_FIX) {
|
||||
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
|
||||
if let Some(diff) = Diff::from_message(message) {
|
||||
writeln!(writer, "{diff}")?;
|
||||
}
|
||||
@@ -234,39 +241,35 @@ impl Display for MessageCodeFrame<'_> {
|
||||
}
|
||||
|
||||
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
|
||||
static TAB_SIZE: u32 = 4; // TODO(jonathan): use `pycodestyle.tab-size`
|
||||
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
|
||||
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
let mut range = annotation_range;
|
||||
let mut column = 0;
|
||||
let mut line_width = LineWidth::new(TAB_SIZE);
|
||||
|
||||
for (index, c) in source.chars().enumerate() {
|
||||
match c {
|
||||
'\t' => {
|
||||
let tab_width = TAB_SIZE - column % TAB_SIZE;
|
||||
column += tab_width;
|
||||
for (index, c) in source.char_indices() {
|
||||
let old_width = line_width.get();
|
||||
line_width = line_width.add_char(c);
|
||||
|
||||
if index < usize::from(annotation_range.start()) {
|
||||
range += TextSize::new(tab_width - 1);
|
||||
} else if index < usize::from(annotation_range.end()) {
|
||||
range = range.add_end(TextSize::new(tab_width - 1));
|
||||
}
|
||||
if matches!(c, '\t') {
|
||||
// SAFETY: The difference is a value in the range [1..TAB_SIZE] which is guaranteed to be less than `u32`.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let tab_width = (line_width.get() - old_width) as u32;
|
||||
|
||||
result.push_str(&source[last_end..index]);
|
||||
|
||||
for _ in 0..tab_width {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
last_end = index + 1;
|
||||
if index < usize::from(annotation_range.start()) {
|
||||
range += TextSize::new(tab_width - 1);
|
||||
} else if index < usize::from(annotation_range.end()) {
|
||||
range = range.add_end(TextSize::new(tab_width - 1));
|
||||
}
|
||||
'\n' | '\r' => {
|
||||
column = 0;
|
||||
}
|
||||
_ => {
|
||||
column += 1;
|
||||
|
||||
result.push_str(&source[last_end..index]);
|
||||
|
||||
for _ in 0..tab_width {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
last_end = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,9 +295,10 @@ struct SourceCode<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::TextEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
|
||||
@@ -24,7 +24,6 @@ static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Directive<'a> {
|
||||
@@ -46,12 +45,12 @@ pub(crate) fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator)
|
||||
caps.name("trailing_spaces"),
|
||||
) {
|
||||
(Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => {
|
||||
let codes: Vec<&str> = SPLIT_COMMA_REGEX
|
||||
.split(codes.as_str().trim())
|
||||
let codes = codes
|
||||
.as_str()
|
||||
.split(|c: char| c.is_whitespace() || c == ',')
|
||||
.map(str::trim)
|
||||
.filter(|code| !code.is_empty())
|
||||
.collect();
|
||||
|
||||
.collect_vec();
|
||||
let start = range.start() + TextSize::try_from(noqa.start()).unwrap();
|
||||
if codes.is_empty() {
|
||||
#[allow(deprecated)]
|
||||
@@ -105,11 +104,11 @@ fn parse_file_exemption(line: &str) -> ParsedExemption {
|
||||
if remainder.is_empty() {
|
||||
return ParsedExemption::All;
|
||||
} else if let Some(codes) = remainder.strip_prefix(':') {
|
||||
let codes: Vec<&str> = SPLIT_COMMA_REGEX
|
||||
.split(codes.trim())
|
||||
let codes = codes
|
||||
.split(|c: char| c.is_whitespace() || c == ',')
|
||||
.map(str::trim)
|
||||
.filter(|code| !code.is_empty())
|
||||
.collect();
|
||||
.collect_vec();
|
||||
if codes.is_empty() {
|
||||
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//! Registry of all [`Rule`] implementations.
|
||||
|
||||
mod rule_set;
|
||||
|
||||
use strum_macros::{AsRefStr, EnumIter};
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::RuleNamespace;
|
||||
pub use rule_set::{RuleSet, RuleSetIterator};
|
||||
|
||||
use crate::codes::{self, RuleCodePrefix};
|
||||
use crate::rules;
|
||||
pub use rule_set::{RuleSet, RuleSetIterator};
|
||||
|
||||
mod rule_set;
|
||||
|
||||
ruff_macros::register_rules!(
|
||||
// pycodestyle errors
|
||||
@@ -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};
|
||||
|
||||
@@ -7,7 +7,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -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"])
|
||||
{
|
||||
|
||||
@@ -3,8 +3,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Stmt};
|
||||
use ruff_python_ast::cast;
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::definition::{Definition, Member, MemberKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_python_semantic::model::SemanticModel;
|
||||
|
||||
pub(super) fn match_function_def(
|
||||
stmt: &Stmt,
|
||||
@@ -37,14 +36,14 @@ pub(super) fn match_function_def(
|
||||
}
|
||||
|
||||
/// Return the name of the function, if it's overloaded.
|
||||
pub(crate) fn overloaded_name(checker: &Checker, definition: &Definition) -> Option<String> {
|
||||
pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> Option<String> {
|
||||
if let Definition::Member(Member {
|
||||
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
|
||||
stmt,
|
||||
..
|
||||
}) = definition
|
||||
{
|
||||
if visibility::is_overload(&checker.ctx, cast::decorator_list(stmt)) {
|
||||
if visibility::is_overload(model, cast::decorator_list(stmt)) {
|
||||
let (name, ..) = match_function_def(stmt);
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
@@ -58,7 +57,7 @@ pub(crate) fn overloaded_name(checker: &Checker, definition: &Definition) -> Opt
|
||||
/// Return `true` if the definition is the implementation for an overloaded
|
||||
/// function.
|
||||
pub(crate) fn is_overload_impl(
|
||||
checker: &Checker,
|
||||
model: &SemanticModel,
|
||||
definition: &Definition,
|
||||
overloaded_name: &str,
|
||||
) -> bool {
|
||||
@@ -68,7 +67,7 @@ pub(crate) fn is_overload_impl(
|
||||
..
|
||||
}) = definition
|
||||
{
|
||||
if visibility::is_overload(&checker.ctx, cast::decorator_list(stmt)) {
|
||||
if visibility::is_overload(model, cast::decorator_list(stmt)) {
|
||||
false
|
||||
} else {
|
||||
let (name, ..) = match_function_def(stmt);
|
||||
|
||||
@@ -8,9 +8,9 @@ pub mod settings;
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::assert_messages;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
use crate::test::test_path;
|
||||
|
||||
@@ -8,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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"])
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
)));
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user